HTTP in Swift, Part 2: Basic Structures

Part 2 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

In the previous post, we took a look at the structure of HTTP requests and responses. In this post, we’ll transform that information into the types we need to model them in Swift.

Requests and Responses

As we saw, HTTP is a simple request/response model. For every request, you get a single response. Even things like redirection follow this as well. For example, let’s send a request for a specific endpoint at the Star Wars API:

GET /api HTTP/1.1
Host: swapi.dev
...

The response we get back indicates that the thing we’re looking for has moved to a new location:

HTTP/1.1 301 MOVED PERMANENTLY
Location: http://swapi.dev/api/
...

Many networking stacks will see that 301 MOVED PERMANENTLY response, extract the value in the Location header, and then send a new request to the indicated location:

GET /api/ HTTP/1.1
Host: swapi.dev
...

So even something like redirection still follows the “one request, one response” model. This makes it straight-forward to know how to proceed.

Requests

Based on what we learned last time, there are a few pieces of information we’ll need to know in order to properly form an HTTPRequest. We’re going to build our own type rather than relying on URLRequest for a couple of reasons:

  1. We get a chance to interpose our own logic to expand or contract the functionality provided by URLRequest. For example, since this is an HTTP API, we probably don’t need to allow customization of the scheme of a URL, and instead always default it to "https".
  2. We can be more selective in the amount of information we need in order to construct a request.

Our HTTPRequest struct will need to represent the 3 key pieces of information of a request:

  1. The request line, which has:
    1. The method
    2. The path/URL
  2. Headers
  3. The body

In Swift, dealing with a fully-formed URL value can be a little tedious when we want to build it up piece by piece, so we’ll use the URLComponents value instead. This gives us:

public struct HTTPRequest {
    private var urlComponents = URLComponents()
    public var method: HTTPMethod = .get // the struct we previously defined
    public var headers: [String: String] = [:]
    public var body: Data?

    public init() {
        urlComponents.scheme = "https"
    }
}

I’ve left the urlComponents as private, so that we can selectively re-expose portions of it, such as:

public extension HTTPRequest {

    public var scheme: String { urlComponents.scheme ?? "https" }
    
    public var host: String? {
        get { urlComponents.host }
        set { urlComponents.host = newValue }
    }
    
    public var path: String {
        get { urlComponents.path }
        set { urlComponents.path = newValue }
    }

}

This way we can hide things that we probably won’t need (perhaps the .port? Or the .query string that represents the composed query items), and re-expose them if they’re needed. We also have explicit getters and setters for performing additional validating logic before storing the underlying values.

For now, we’ll represent the headers as a simple Dictionary<String, String>. It’s not exactly correct as header names are technically case insensitive (ie, Content-Type is the same as cOnTeNt-TyPe) and Dictionary doesn’t support that, but we can retrofit that functionality later.

The body we’ll define as an Optional<Data>, because, well, the body of a request is raw binary data that’s optional. 😁 We’ll be changing this in the future to support more kinds of things.

Responses

The structure of a response is very similar to a request, as we’ve already established. And like with URLComponents, there’s an existing type we can use to do a lot of the heavy lifting for us: HTTPURLResponse. HTTPURLResponse represents all of a response except the body. Since we’re going with a simple request/response model, we want a type that can also hold the body:

public struct HTTPResponse {
    public let request: HTTPRequest
    private let response: HTTPURLResponse
    public let body: Data?
    
    public var status: HTTPStatus {
        // A struct of similar construction to HTTPMethod
        HTTPStatus(rawValue: response.statusCode)
    }

    public var message: String {
        HTTPURLResponse.localizedString(forStatusCode: response.statusCode)
    }

    public var headers: [AnyHashable: Any] { response.allHeaderFields }
}

Like with HTTPRequest, this HTTPResponse struct wraps the system-provided types so that we can selectively expose and enhance functionality. One readily apparent difference is that the headers are typed as [AnyHashable: Any], which is different from the [String: String] we used in the request. This is an artifact of the case insensitivity mentioned earlier, and is another opportunity for improvement later.

You might consider also including a copy of the request in the response (as I have), since they are an intertwined pair of data. It can be useful to have the request at hand when examining a response.

Results

The overall form of the request and the response map directly to the form dictated by the HTTP specification. We’ll be extending and improving them as the framework and functionality matures.

One immediate observation we can make is that we don’t always get a response. If we send a request while the user is offline, or if the connection drops before a response can be fully received, then we’ll end up in a situation where we either have no response, or a partial response.

Thus, it’s worthwhile to also define a result type that encapsulates these scenarios. Fortunately, Swift has a nice Result type built-in, which we can repurpose without modification:

public typealias HTTPResult = Result<HTTPResponse, Error>

We could leave it like that, but I think we can do a bit better with that Error type. So, we’ll create an HTTPError type and alter the typealias to use that:

public typealias HTTPResult = Result<HTTPResponse, HTTPError>

public struct HTTPError: Error {
    /// The high-level classification of this error
    public let code: Code

    /// The HTTPRequest that resulted in this error
    public let request: HTTPRequest

    /// Any HTTPResponse (partial or otherwise) that we might have
    public let response: HTTPResponse?

    /// If we have more information about the error that caused this, stash it here
    public let underlyingError: Error?

    public enum Code {
        case invalidRequest     // the HTTPRequest could not be turned into a URLRequest
        case cannotConnect      // some sort of connectivity problem
        case cancelled          // the user cancelled the request
        case insecureConnection // couldn't establish a secure connection to the server
        case invalidResponse    // the system did not receive a valid HTTP response
        ...                     // other scenarios we may wish to expose; fill them in as necessary
        case unknown            // we have no idea what the problem is
    }
}

The last thing we notice is that if we have an HTTPResult, then we will always have an HTTPRequest, either because this is a .success case or because we have the error’s .request. Also, we may likely have a response as well. Thus we can extend our typealias to make extract these pieces easy:

extension HTTPResult {
    
    public var request: HTTPRequest {
        switch self {
            case .success(let response): return response.request
            case .failure(let error): return error.request
        }
    }
    
    public var response: HTTPResponse? {
        switch self {
            case .success(let response): return response
            case .failure(let error): return error.response
        }
    }
    
}

These three types (HTTPRequest, HTTPResponse, and HTTPError) form the basis of everything we’ll need going forward.

In the next post, we’ll take a closer look at the body of the request and modify it to support more complex behaviors.


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 4: Loading Requests
HTTP in Swift, Part 3: Request Bodies
HTTP in Swift, Part 1: An Intro to HTTP