HTTP in Swift, Part 2: Basic Structures
Part 2 in a series on building a Swift HTTP framework:
- HTTP in Swift, Part 1: An Intro to HTTP
- HTTP in Swift, Part 2: Basic Structures
- HTTP in Swift, Part 3: Request Bodies
- HTTP in Swift, Part 4: Loading Requests
- HTTP in Swift, Part 5: Testing and Mocking
- HTTP in Swift, Part 6: Chaining Loaders
- HTTP in Swift, Part 7: Dynamically Modifying Requests
- HTTP in Swift, Part 8: Request Options
- HTTP in Swift, Part 9: Resetting
- HTTP in Swift, Part 10: Cancellation
- HTTP in Swift, Part 11: Throttling
- HTTP in Swift, Part 12: Retrying
- HTTP in Swift, Part 13: Basic Authentication
- HTTP in Swift, Part 14: OAuth Setup
- HTTP in Swift, Part 15: OAuth
- HTTP in Swift, Part 16: Composite Loaders
- HTTP in Swift, Part 17: Brain Dump
- HTTP in Swift, Part 18: Wrapping Up
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:
- 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 thescheme
of a URL, and instead always default it to"https"
. - 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:
- The request line, which has:
- The method
- The path/
URL
- Headers
- 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.