- 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 theURLQueryItem
is 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 stream
The .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: JSONEncoder
to serialize anEncodable
object as the body of the request..json(_ object: Any, options: JSONSerialization.WritingOptions = [])
uses theJSONSerialization
class
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 used
At 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
: theHTTPURLResponse
receivedstatusCode
: 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)