In addition to the ongoing discussions of the lowest level networking types like IP address, we'd like to start discussions around the very top of the stack, the HTTPClient. This is a large design space with many existing solutions in Swift and across all programming languages. For the purposes of this thread we'd like to cover some specific topics. Feel free to bring up additional questions and we can continue discussion and spin off other threads, which will be linked back here. Current topics include:
- What is
HTTPClient, how is it created, and how is it used? - What is the top level interface to performing a request?
- How are requests configured?
- How do users handle responses?
There is currently a prototype implementation of an existential HTTPClient that wraps various platform HTTP libraries in swift-http-api-proposal. Please note that this library is an experimental prototype created to explore various API designs, not a final proposal, but it contains a functional cross platform implementation that illustrates the possible shape of a Swift HTTP library.
HTTP Client
Fundamentally, an HTTP client is a state isolation boundary where the common state for some set of requests is kept. This (possibly) includes things like the connection pool and various bits of storage like caches or cookies.
The nature of the top-level HTTPClient type is a big part of a user's initial impression of the library. Is it a protocol or a concrete type? Is it a class, a struct, or an actor? The experimental API uses an HTTPClient protocol, as well as the concrete DefaultHTTPClient class which conforms to the protocol and uses various platform APIs to actually make requests.
Performing a request
At its simplest, what does it look like to perform a request? Can we generalize all requests into a single interface or are there multiple fundamental shapes we must support? Currently, the prototype HTTPClient protocol has a single requirement:
func perform<Return: ~Copyable>(
request: HTTPRequest,
body: consuming HTTPClientRequestBody<Writer>?,
options: HTTPRequestOptions,
responseHandler: (HTTPResponse, consuming Reader) async throws -> Return
) async throws -> Return
Configuring requests
As you can see in the above example, request options are passed in the perform method as HTTPRequestOptions, which is actually an associatedtype from the client protocol conforming to an HTTPCapabilities.RequestOptions protocol that currently has no requirements. This allows conformers to provide their own options. However, there is a broader question here: do options exist only at the perform level, or can they exist at the HTTPClient level as well? How are they split? How do they override each other? What values are necessary to configure a request?
Creating a body
perform uses an HTTPClientRequestBody with an abstract Writer provided by the conformer. This enables streaming bodies in requests with APIs like
body: .seekable { offset, writer in
var requestBody = // produce the request body
try await writer.write(buffer: &requestBody)
}
What should users be able to do with the body of the request?
Handling responses
As you can see by the perform, returning a single Return value is one possible shape, where the consuming Reader allows callers to perform their parsing on an AsyncReader of UInt8 values. This async stream will likely be a fundamental construct of any Swift network API, but is this the appropriate, foundational interface? Are there requests that need something more? Can we build all of the needed higher level API on this function?
These are just some of the many questions we must explore in this space as Swift builds out its own networking capabilities. We look forward to your feedback.