- Build & Execute a Request
RealHTTP offers a type-safe, perfectly Swift integrated way to build and configure a new http request.
At the simplest, you just need to provide a valid url, either as a URL or a String (conversion happens automatically):
let todo = try await HTTPRequest("https://jsonplaceholder.typicode.com/todos/1")
.fetch(Todo.self)The preceding code builds a GET HTTPRequest and executes it into the shared HTTPClient instance.
The result is then converted into a Todo object via the Decodable protocol.
All asynchronously, all in one line of code.
However not all requests are so simple; you may need to configure parameters, headers, and the body, or even some other settings like timeout or retry/cache strategies. We'll look at this below.
You have three different convenient ways to create a new request depending how many settings you want to change.
You can use this method when your configuration is pretty simple, just the HTTP method and the absolute URL.
This example creates a post to add a new todo to the jsonplaceholder site using automatic json conversion (you will learn more about body encoding below).
let req = try HTTPRequest(method: .post, "https://jsonplaceholder.typicode.com/posts",
body: try .json(["title": "foo", "body": "bar", "userId": 1]))
let _ = try await req.fetch()RealHTTP also allows you to create a request via URI Template, as specified by RFC6570 using the Kylef Swift implementation.
A URI Template is a compact sequence of characters for describing a range of Uniform Resource Identifiers through variable expansion.
let req = try HTTPRequest(URI: "https://jsonplaceholder.typicode.com/posts/{postId}",
variables: ["postId": 1])
let _ = try await req.fetch()The most complete way to configure a request is by using the builder pattern initialization. It allows you to specify any property of the HTTPRequest inside a callback function which encapsulates and makes clear the init process.
let req = HTTPRequest {
// Setup default params
$0.url = URL(string: "https://.../login")!
$0.method = .get
$0.timeout = 100
// Setup some additional settings
$0.redirectMode = redirect
$0.maxRetries = 4
$0.allowsCellularAccess = false
// Setup URL query params & body
$0.addQueryParameter(name: "full", value: "1")
$0.addQueryParameter(name: "autosignout", value: "30")
$0.body = .json(["username": username, "pwd": pwd])
}
let _ = try await req.fetch()You can configure the behavior and settings of your request directly inside the callback, as shown above.
You can add URL query parameters in several different ways:
req.add(parameters: [String: Any])allows you to append a dictionary of String/Any objects to your query. It also allows you to specify how to encode array values (by default.withBrackets) and boolean values (by default.asNumbers).req.add(parameters: [String: String])if your dictionary is just a map of String, String.
Or you can pass directly the URLQueryItem instances via:
req.add(queryItems: [URLQueryItem])req.add(queryItem: URLQueryItem)req.addQueryParameter(name: String, value: String): in this case theURLQueryItemis created from passed parameters.
For example:
let req = HTTPRequest {
$0.url = URL(string: "https://.../login")!
$0.add(parameters: ["username": "Michael Bublé", "pwd": "abc", "autosignout": true])
$0.addQueryParameter(name: "full", value: "1")will produce the following url: https://.../login?username=Michael+Bubl%C3%A9&pwd=abc&autosignout:1&full1.
As you can see, values are encoded automatically, including percent escape and utf8 characters (emoji are supported!).
Request's headers can be set using the req.header = property which requires an HTTPHeader object.
This object is just a type-safe interface to set headers; you can use one of the preset keys by passing one of the valid enum values, or add your own by passing a plain string:
let req = HTTPRequest(...)
req.headers = HTTPHeaders([
.init(name: "X-API-Key", value: "abc"), // custom key
.init(name: .userAgent, value: "MyCoolApp"), // preset key
.init(name: .cacheControl, value: HTTPCacheControl.noTransform.headerValue)
])Values are eventually combined with the destination HTTPClient's headers to produce a final list of headers to send (the request's headers takes the precedence and may override default headers from the client).
The body must conform to the HTTPBody protocol.
RealHTTP provides several built-in types that conform to this protocol in order to simplify your setup.
Specifically, when you call req.body = ... you can use one of the following options.
To set the body of a request for URL query parameter forms (application/x-www-form-urlencoded;) you can use the .formURLEncodedBody(_ parameters: [String: Any]) method:
let req = HTTPRequest(...)
req.body = .formURLEncodedBody(["username": "Michael Bublé", "pwd": "abc"])
// Will produce a body with this string: pwd=abc&username=Michael%20Bubl%C3%A9
// and content type headers `application/x-www-form-urlencoded;`To set a raw Data object as the body, call the .data(_ content: Data, contentType mimeType: MIMEType) method. It allows you to specify the content-type from a preset list of MIMEType objects.
It supports streams (NSInputStream) both from Data or file URL:
// Different set of raw data
req.body = .data(someData, contentType: .gzip) // some gizip raw data
req.body = .data(.data(someData), contentType: .otf) // otf font raw data
req.body = .data(.fileURL(localFileURL), contentType: .zip) // if you have big data you can transfer it via streamThe .string(_ content: String, contentType: MIMEType) encodes a plain string as the body, along with the specified content-type (default is text/plain).
req.body = .string("😃😃😃", contentType: .html)RealHTTP natively supports JSON data. You can use any Encodable conforming object or any object which can be transformed with the built-in JSONSerialization:
.json<T: Encodable>(_ object: T, encoder: JSONEncoderto serialize anEncodableobject as the body of the request..json(_ object: Any, options: JSONSerialization.WritingOptions = [])uses theJSONSerializationclass
public struct UserCredentials: Codable {
var username: String
var pwd: String
}
let credentials = UserCredentials(username: "", pwd: "abc")
let req = HTTPRequest(...)
req.body = try .json(credentials)It will produce a body with the following JSON:
{"pwd":"abc","username":"Michael Bublé"}RealHTTP also supports Multipart Form Data construction with an easy-to-use form builder which supports: Key/Value entries, Local URL files, and Streams!
let req = HTTPRequest(method: .post, URL: ...)
req.body = try .multipart(boundary: nil, { form in
// Key/Value support
try form.add(string: "320x240", name: "size")
try form.add(string: "Michael Bublé", name: "author")
// Local file URL support
try form.add(fileURL: credentialsFileURL, name: "credentials")
// Data stream support
try form.add(fileStream: localFileURL, headers: .init())
})Once you have configured a request you're ready to execute it.
In order to be executed, a request must be passed to a client. The class HTTPClient represents a container of common configuration settings which can manage a session.
For example, a client can be configured to use a base URL for each request (you will not set the url inside the request configuration, just the path), in order to send a common set of headers for each request executed.
It also manages received/sent cookies.
Under the hood, the client is a queue, so you can also set the maximum number of concurrent connections (if not specified, the OS will do it according to the available resources).
HTTPClient also contains validators: validators are chainable pieces of code which are used to validate the response of a request and decide whether to use an optional retry strategy, return with an error, or accept the server data.
You can use this object to create your common web service validation logic instead of duplicating your code (see the section "Advanced HTTPClient" for more info).
HTTPClient.shared is the shared client. No baseURL is set for the shared client, so your request must contain the absolute url (via the url parameter) in order to be executed correctly.
When you call the fetch() method without passing a client, the shared client is used.
// Full URL is required to execute request in shared client
let req = try HTTPRequest(method: .post, "https://jsonplaceholder.typicode.com/posts")
let _ = try await req.fetch() // if not specified, HTTPClient.shared is usedAt times, you may need to take more control over your client or isolate specific application logic.
For example, we use different clients for communicating with B2B vs B2C web services.
This allows us to have fine-grained control over our settings (cookies, session management, concurrent operations and more).
The following example creates a new client with some settings:
public lazy var b2cClient: HTTPClient = {
var config = URLSessionConfiguration.default
config.httpShouldSetCookies = true
config.networkServiceType = .responsiveData
let client = HTTPClient(baseURL: "https://myappb2c.ws.org/api/v2/",configuration: config)
// Setup some common HTTP Headers for all requests
client.headers = HTTPHeaders([
.init(name: .userAgent, value: myAgent),
.init(name: "X-API-Experimental", value: "true")
])
return client
}()Now we can use it to perform a new request:
let loginCredentials = UserCred(username: "..." pwd: "...") // conforms to Encodable
let req = HTTPRequest {
$0.path = "login" // full url will be b2cClient.baseURL + path
$0.method = .post
$0.addQueryParameter(name: "autosignout", value: "30")
$0.body = .json(loginCredentials) // automatic conversion to json in body
}
// URL is: https://myappb2c.ws.org/api/v2/login?autosignout=30
// Execute async request and decode the response to LoggedUser object (Codable).
let user = req.fetch(b2cClient).decode(LoggedUser.self)As shown above, executing an asynchronous request is as easy as calling its fetch() method.
This is an async throwable method, so you need to call it in an async scope.
The following is an example that uses the Task and @MainActor to execute an async request and update the UI on main thread:
let task = detach {
do {
let user = req.fetch(b2cClient).decode(LoggedUser.self)
self.updateUserProfile(.success(user))
} catch {
self.updateUserProfile(.failure(error))
}
}
@MainActor
private func updateUserProfile(_ data: Result<LoggedUser,Error>) {
// executed on main thread
}These topics are not related to the http library, so if you would like more information, check out some of the @MainActor docs (here, here or here).
You may want to intercept the moment where the destination client produces a URLRequest instance from an HTTPRequest in order to alter some values. RealHTTP offers the urlRequestModifier method to intercept and modify the request.
In this example we remove some headers and disable the execution when on a cellular network:
let req = HTTPRequest(...)
req.urlRequestModifier = { request in
request.allowsCellularAccess = false
request.headers.remove(name: .cacheControl)
request.headers.remove(name: "X-API-Key")
}As any other async operation you can force the library to cancel a running request. This may be due to not needing that resource anymore or due to some other constraints in your app lifecycle.
In all of these cases, use the cancel() method to stop the request and ignore the response.
let req = HTTPRequest(...)
let res = try await req.fetch()
// Somewhere in your code from another thread
res.cancel()Once fetch() is done you will get an HTTPResponse object which contains the raw response from the server.
This object contains some interesting properties:
data: the raw body received (asData)metrics: collected URL metrics during the request (HTTPMetrics)httpResponse: theHTTPURLResponsereceivedstatusCode: the HTTP Status Code receivederror: if an error has occured, you can find the details hereheaders: received headers (HTTPHeaders)
You don't usually want to handle the raw response, but would rather transform it into a typed object.
HTTPResponse provides several decode() methods you can use to transform raw data into something useful:
The decode<T: HTTPDecodableResponse>() method allows you to transform the response into an object that conforms to HTTPDecodableResponse.
HTTPDecodableResponse is automatically implemented by Decodable so any object that conforms to the Decodable or Codable protocol can be transformed automatically.
Moreover, if you need to perform a custom decoding (ie using SwiftyJSON or other libraries), you can conform your object to HTTPDecodableResponse and implement the only required method:
import SwiftyJSON
public struct MyUser: HTTPDecodableResponse {
var name: String
var age: Int
// Implement your own logic to decode a custom object.
// You can return `nil`, your instance, or throw an error if needed.
public static func decode(_ response: HTTPResponse) throws -> RequestsTests.MyUser? {
let json = JSON(data: response.data)
guard json["isValid"].boolValue else {
throw Error("Invalid object")
}
return MyUser(name: json["fullName"].stringValue, age: json["age"].intValue)
}
}Whether you are using a type that conforms to Codable or to HTTPDecodableResponse, you just need to call decode():
let user: MyUser? = try await loginUser(user: "mark", pwd: "...").fetch().decode(MyUser.self)Et voilà!
To transform a raw response to a JSON object using JSONSerialization class you just need to call decode() by passing your object and, optionally, an options parameter:
let req = try HTTPRequest(...)
let result = try await req.fetch(newClient).decodeJSONData([String: Any].self, options: .fragmentsAllowed)