Skip to content

Latest commit

 

History

History
386 lines (287 loc) · 16.2 KB

2.Build_Request.md

File metadata and controls

386 lines (287 loc) · 16.2 KB

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.

Initialize a Request

You have three different convenient ways to create a new request depending how many settings you want to change.

Standard

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()

URI Template

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()

Builder Pattern

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.

Setup Query Parameters

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 the URLQueryItem 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!).

Setup Headers

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).

Setup Request Body

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.

URL Query Parameters

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;`

Raw Data & Stream

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

Plain Strings

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)

JSON Data

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 an Encodable object as the body of the request.
  • .json(_ object: Any, options: JSONSerialization.WritingOptions = []) uses the JSONSerialization 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é"}

Multipart-Form-Data

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())
})

The HTTP Client

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).

Shared Client

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

Custom Client

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)

Execute a Request

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).

Modify a Request

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")
}

Cancel a Request

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()

The HTTP Response

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 (as Data)
  • metrics: collected URL metrics during the request (HTTPMetrics)
  • httpResponse: the HTTPURLResponse received
  • statusCode: the HTTP Status Code received
  • error: if an error has occured, you can find the details here
  • headers: 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:

Decode using Codable & Custom Decoding

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à!

Decode Raw JSON using JSONSerialization

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)