Async HTTP API shoppers in Swift


Introducing SwiftHttp

An superior Swift HTTP library to quickly create communication layers with API endpoints. The library tries to separate the shopper request logic from the request constructing and response dealing with. That is the primary purpose why it has a HttpClient protocol which can be utilized to carry out knowledge, obtain and add duties. You’ll be able to implement your personal HttpClient, however SwiftHttp comes with a built-in UrlSessionHttpClient primarily based on Basis networking.

So the shopper is accountable for executing the requests, however we nonetheless have to explain the request itself by some means. That is the place the HttpRawRequest object comes into play. You’ll be able to simply create a base HttpUrl and carry out a request utilizing the HttpRawRequest object. When working with a uncooked request you may specify further header fields and a uncooked physique knowledge object too. 💪

let url = HttpUrl(scheme: "https",
                  host: "jsonplaceholder.typicode.com",
                  port: 80,
                  path: ["todos"],
                  useful resource: nil,
                  question: [:],
                  fragment: nil)

let req = HttpRawRequest(url: url, methodology: .get, headers: [:], physique: nil)


let shopper = UrlSessionHttpClient(session: .shared, log: true)
let response = strive await shopper.dataTask(req)


let todos = strive JSONDecoder().decode([Todo].self, from: response.knowledge)

The HTTP shopper can carry out community calls utilizing the brand new async / await Swift concurrency API. It’s attainable to cancel a community request by wrapping it right into a structured concurrency Activity.

let job = Activity {
    let api = TodoApi()
    _ = strive await api.record()
}

DispatchQueue.world().asyncAfter(deadline: .now() + .milliseconds(10)) {
    job.cancel()
}

do {
    let _ = strive await job.worth
}
catch {
    if (error as? URLError)?.code == .cancelled {
        print("cancelled")
    }
}

This can be a neat tick, you too can verify the rationale contained in the catch block, whether it is an URLError with a .cancelled code then the request was cancelled, in any other case it should be some form of community error.

So that is how you should utilize the shopper to carry out or cancel a community job, however often you do not wish to work with uncooked knowledge, however encodable and decodable objects. If you work with such objects, you would possibly wish to validate the response headers and ship further headers to tell the server about the kind of the physique knowledge. Simply take into consideration the Content material-Sort / Settle for header fields. 🤔

So we’d wish to ship further headers alongside the request, plus it would be good to validate the standing code and response headers earlier than we attempt to parse the information. This looks like a movement of frequent operations, first we encode the information, set the extra header fields, and when the response arrives we validate the standing code and the header fields, lastly we attempt to decode the information object. This can be a typical use case and SwiftHttp calls this workflow as a pipeline.

There are 4 sorts of built-in HTTP pipelines:

  • Uncooked – Ship a uncooked knowledge request, return a uncooked knowledge response
  • Encodable – Ship an encodable object, return a uncooked knowledge response
  • Decodable – Ship a uncooked knowledge request, return a decodable object
  • Codable – Ship an encodable object, return a decodable object

We will use a HttpRawPipeline and execute our request utilizing a shopper as an executor.

let baseUrl = HttpUrl(host: "jsonplaceholder.typicode.com")
let shopper = UrlSessionHttpClient(session: .shared, log: true)

let pipeline = HttpRawPipeline(url: baseUrl.path("todos"), methodology: .get)

let response = strive await pipeline.execute(shopper.dataTask)
let todos = strive JSONDecoder().decode([Todo].self, from: response.knowledge)
print(response.statusCode)
print(todos.rely)

On this case we had been utilizing the dataTask perform, however should you anticipate the response to be an enormous file, you would possibly wish to think about using a downloadTask, or should you’re importing a considerable amount of knowledge when sending the request, it is best to select the uploadTask perform. 💡

So on this case we needed to manually decode the Todo object from the uncooked HTTP response knowledge, however we are able to use the decodable pipeline to make issues much more easy.

let baseUrl = HttpUrl(host: "jsonplaceholder.typicode.com")
let shopper = UrlSessionHttpClient(session: .shared, log: true)


let pipeline = HttpDecodablePipeline<[Todo]>(url: baseUrl.path("todos"),
                                             methodology: .get,
                                             decoder: .json(JSONDecoder(), validators: [
                                                HttpStatusCodeValidator(.ok),
                                                HttpHeaderValidator(.key(.contentType)) {
                                                    $0.contains("application/json")
                                                },
                                             ]))

let todos = strive await pipeline.execute(shopper.dataTask)
print(todos.rely)

As you may see, on this case the as an alternative of returning the response, the pipeline can carry out further validation and the decoding utilizing the supplied decoder and validators. You’ll be able to create your personal validators, there’s a HttpResponseValidator protocol for this objective.

The encodable pipeline works like the identical, you may specify the encoder, you may present the encodable object and you will get again a HttpResponse occasion.

let shopper = UrlSessionHttpClient(session: .shared, log: true)
        
let todo = Todo(id: 1, title: "lorem ipsum", accomplished: false)

let pipeline = HttpEncodablePipeline(url: baseUrl.path("todos"),
                                     methodology: .publish,
                                     physique: todo,
                                     encoder: .json())

let response = strive await pipeline.execute(shopper.dataTask)

print(response.statusCode == .created)

The codable pipeline is a mixture of the encodable and decodable pipeline. 🙃

let baseUrl = HttpUrl(host: "jsonplaceholder.typicode.com")
let shopper = UrlSessionHttpClient(session: .shared, log: true)

let todo = Todo(id: 1, title: "lorem ipsum", accomplished: false)

let pipeline = HttpCodablePipeline<Todo, Todo>(url: baseUrl.path("todos", String(1)),
                                               methodology: .put,
                                               physique: todo,
                                               encoder: .json(),
                                               decoder: .json())

let todo = strive await pipeline.execute(shopper.dataTask)
print(todo.title)

As you may see that is fairly a standard sample, and once we’re speaking with a REST API, we will carry out roughly the very same community calls for each single endpoint. SwiftHttp has a pipeline assortment protocol that you should utilize to carry out requests with out the necessity of explicitly establishing these pipelines. This is an instance:

import SwiftHttp

struct Todo: Codable {
    let id: Int
    let title: String
    let accomplished: Bool
}

struct TodoApi: HttpCodablePipelineCollection {

    let shopper: HttpClient = UrlSessionHttpClient(log: true)
    let apiBaseUrl = HttpUrl(host: "jsonplaceholder.typicode.com")

    
    func record() async throws -> [Todo] {
        strive await decodableRequest(executor: shopper.dataTask,
                                   url: apiBaseUrl.path("todos"),
                                   methodology: .get)
    }    
}

let todos = strive await api.record()

When utilizing a HttpCodablePipelineCollection you may carry out an encodable, decodable or codable request utilizing an executor object. This can scale back the boilerplate code wanted to carry out a request and all the things goes to be sort protected due to the generic protocol oriented networking layer. You’ll be able to setup as many pipeline collections as you want, it’s attainable to make use of a shared shopper or you may create a devoted shopper for every.

By the best way, if one thing goes improper with the request, or one of many validators fail, you may all the time verify for the errors utilizing a do-try-catch block. 😅

do {
    _ = strive await api.record()
}
catch HttpError.invalidStatusCode(let res) {
    
    let decoder = HttpResponseDecoder<CustomError>(decoder: JSONDecoder())
    do {
        let error = strive decoder.decode(res.knowledge)
        print(res.statusCode, error)
    }
    catch {
        print(error.localizedDescription)
    }
}
catch {
    print(error.localizedDescription)
}

That is how SwiftHttp works in a nutshell, after all you may setup customized encoders and decoders, however that is one other matter. In case you are within the undertaking, be happy to offer it a star on GitHub. We will use it sooner or later quite a bit each on the shopper and server aspect. ⭐️⭐️⭐️

Recent Articles

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here

Stay on op - Ge the daily news in your inbox