HTTP in Swift, Part 4: Loading Requests

Part 4 in a series on building a Swift HTTP framework:

  1. HTTP in Swift, Part 1: An Intro to HTTP
  2. HTTP in Swift, Part 2: Basic Structures
  3. HTTP in Swift, Part 3: Request Bodies
  4. HTTP in Swift, Part 4: Loading Requests
  5. HTTP in Swift, Part 5: Testing and Mocking
  6. HTTP in Swift, Part 6: Chaining Loaders
  7. HTTP in Swift, Part 7: Dynamically Modifying Requests
  8. HTTP in Swift, Part 8: Request Options
  9. HTTP in Swift, Part 9: Resetting
  10. HTTP in Swift, Part 10: Cancellation
  11. HTTP in Swift, Part 11: Throttling
  12. HTTP in Swift, Part 12: Retrying
  13. HTTP in Swift, Part 13: Basic Authentication
  14. HTTP in Swift, Part 14: OAuth Setup
  15. HTTP in Swift, Part 15: OAuth
  16. HTTP in Swift, Part 16: Composite Loaders

Up until now, we’ve been looking at the structure and implementation of a simple request/response model. Now it’s time to talk about how we send requests and receive responses.

If we think back to the first post, we’ll recall that with HTTP, we send off a request and eventually get a response (ignoring errors for a moment). There’s nothing about “tasks” or delegates or anything. We send (or “load”) a request. We eventually get a response.

If we were to describe that functionality in terms of a function, it would look something like this:

func load(request: HTTPRequest, completion: @escaping (HTTPResponse) -> Void)

We send a request, and at some point in the future, our closure will be executed with whatever response we got back. Of course, a single function isn’t what we want; instead, we want to describe an interface, ie the “shape of a thing”. So, we’ll wrap this up in a protocol:

public protocol HTTPLoading {
    func load(request: HTTPRequest, completion: @escaping (HTTPResponse) -> Void)
}

Of course, we might actually have errors (dropped connections, etc), so we’ll swap out that HTTPResponse for our “result” typealias:

public protocol HTTPLoading {
    func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void)
}

Let’s stop and appreciate this for a moment. This single method is all we’re going to need to define the core functionality of our networking framework. That’s it: one method. That’s awesome.

Let’s implement this single method using URLSession.

Conforming to HTTPLoading

URLSession is where the “rubber meets the road”. It’s the last hurdle to clear before our requests go out over the air (or wires) to the servers we’ve indicated. So it makes sense that our implementation of HTTPLoading on a URLSession will be about translating the HTTPRequest into the URLRequest that the session needs:

extension URLSession: HTTPLoading {
    public func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void)
        guard let url = request.url else {
            // we couldn't construct a proper URL out of the request's URLComponents
            completion(.failure(...))
            return
        }
        
        // construct the URLRequest
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = request.method.rawValue
        
        // copy over any custom HTTP headers
        for (header, value) in request.headers {
            urlRequest.addValue(value, forHTTPHeaderField: header)
        }
        
        if request.body.isEmpty == false {
            // if our body defines additional headers, add them
            for (header, value) in request.body.additionalHeaders {
                urlRequest.addValue(value, forHTTPHeaderField: header)
            }
            
            // attempt to retrieve the body data
            do {
                urlRequest.httpBody = try request.body.encode()
            } catch {
                // something went wrong creating the body; stop and report back
                completion(.failure(...))
                return
            }
        }
        
        let dataTask = session.dataTask(with: urlRequest) { (data, response, error) in
            // construct a Result<HTTPResponse, HTTPError> out of the triplet of data, url response, and url error
            let result = HTTPResult(request: request, responseData: data, response: response, error: error)
            completion(result)
        }
        
        // off we go!
        dataTask.resume()
    }
}

This should be pretty easy to follow. We’re going through the steps to pull information out of our HTTPRequest value and apply it to a URLRequest. If at any point something goes wrong, then we’ll report back with an error. (You’d need to fill in the ... parts yourselves to construct an appropriate HTTPError value)

Assuming that construction all goes smoothly, we end up with a URLRequest that we can turn into a URLSessionDataTask and execute it. When it completes, we’ll take the response values, turn them into an HTTPResult, and report back via the completion block.

Creating the Result

At any point during transmission, our request might fail. If we’re in Airplane Mode or otherwise “unconnected”, then the request might never get sent at all. If we’re in the middle of sending the request (before we’ve gotten a response), the network connection might drop. Or it might drop after we’ve sent, but before we’ve gotten a response. Or it might drop after we’ve started getting the response, but before the response can be fully received.

This is why we created that HTTPError struct while defining the request and response types, and it means we need to be a little bit more diligent about constructing our result than simply checking to see “did I get some Data back”.

At a high level, the logic to initialize an HTTPResult looks roughly like this:

var httpResponse: HTTPResponse?
if let r = response as? HTTPURLResponse {
    httpResponse = HTTPResponse(request: request, response: r, body: responseData ?? Data())
}

if let e = error as? URLError {
    let code: HTTPError.Code
    switch e.code {
        case .badURL: code = .invalidRequest
        case .unsupportedURL: code = ...
        case .cannotFindHost: code = ...
        ...
        default: code = .unknown
    }
    self = .failure(HTTPError(code: code, request: request, response: httpResponse, underlyingError: e))
} else if let someError = error {
    // an error, but not a URL error
    self = .failure(HTTPError(code: .unknown, request: request, response: httpResponse, underlyingError: someError))
} else if let r = httpResponse {
    // not an error, and an HTTPURLResponse
    self = .success(r)
} else {
    // not an error, but also not an HTTPURLResponse
    self = .failure(HTTPError(code: .invalidResponse, request: request, response: nil, underlyingError: error))
}

Usage

Using an HTTPLoading value is simple:

public class StarWarsAPI {
    private let loader: HTTPLoading = URLSession.shared

    public func requestPeople(completion: @escaping (...) -> Void) {
        var r = HTTPRequest()
        r.host = "swapi.dev"
        r.path = "/api/people"

        loader.load(request: r) { result in
            // TODO: interpret the result
            completion(...)
        }
    }
}

I want to point out here that at no point are we interpreting the response’s status code. Getting a 500 Internal Server Error or 404 Not Found response is a successful response. At this layer, “successful” means “we got a response”, and not “the response indicates some sort of semantic error”. Interpreting a status code is app-specific logic. In future posts we will allow for customizable, app-specific behavior that is based on status codes (such as following redirects or retrying requests).


This single method that we’ve defined is deceptively simple, but it’s also not complete. We haven’t indicated any way to proactively cancel a request, and we’ll need to adjust our HTTPLoading protocol to add a couple more pieces of functionality. We’ll also be transforming it from a protocol to a class, for reasons which I’ll explain in future posts.

Despite these small holes, the protocol is still beautiful in its simplicity and it goes to show how a good conceptualization of a problem can result in something powerful and beautiful.

Simplicity is the ultimate sophistication.

In the next post, we’ll look at using the HTTPLoading protocol to simplify unit testing.


Related️️ Posts️

HTTP in Swift, Part 16: Composite Loaders
HTTP in Swift, Part 15: OAuth
HTTP in Swift, Part 14: OAuth Setup
HTTP in Swift, Part 13: Basic Authentication
HTTP in Swift, Part 12: Retrying
HTTP in Swift, Part 11: Throttling
HTTP in Swift, Part 10: Cancellation
HTTP in Swift, Part 9: Resetting
HTTP in Swift, Part 8: Request Options
HTTP in Swift, Part 7: Dynamically Modifying Requests
HTTP in Swift, Part 6: Chaining Loaders
HTTP in Swift, Part 5: Testing and Mocking
HTTP in Swift, Part 3: Request Bodies
HTTP in Swift, Part 2: Basic Structures
HTTP in Swift, Part 1: An Intro to HTTP