HTTP is the internet’s foundational application-layer protocol. In fact, you probably just used it to view this post. Today we want to kick off the effort to design a new unified HTTP Client API to improve the experience for developers targeting multiple platforms, adopting modern Swift features, and supporting new use cases. While the API will be consistent, the underlying implementation may vary by platform today, retaining the potential over the longer term for a common source base. As a part of the ongoing work in the proposed Swift networking vision, we invite your participation as we work together to make Swift the best language for all networking use cases — starting with HTTP.
Goals
These are the goals that we have in mind when designing the API. We’d love to hear your thoughts on the goals and the overall approach to help shape the final design.
Designed for Swift
The new API should be Swift-first, adopting modern language features and best practices. And it must fully embrace Swift’s concurrency model and structured concurrency.
Progressive disclosure with full-featured HTTP support
The API should be simple for common use cases, such as HTTP redirection, metrics, authentication, cookies, and caching. Yet it should also support more advanced use cases, such as trailers, bidirectional streaming, and resumable uploads. Moving from simple to advanced use cases should not require major rewrites.
More than just for app developers
The API should serve more than just app developers. Library developers need the flexibility to avoid depending on a specific concrete client implementation, while middleware developers want to extend existing HTTP client functionality.
Cross-platform
The API should work on all platforms that Swift supports, while allowing platform-specific behaviors.
Performant
The API should allow high-performance implementations expected of a low-level system language.
Current design
The current design is still at its early stages. You can find the prototype implementation here, with more detailed proposals being available in the near future.
The proposal consists of 3 parts: the abstract interface, convenience methods to use the interface, and a concrete platform default implementation.
Abstract interface
Similar to the HTTP server API, the HTTP client API is based on an abstract protocol with a single method perform. Library authors who do not want to depend on a specific HTTP implementation can accept the abstract HTTPClient protocol. The dependency injection pattern allows libraries and apps to be more modular and testable.
public protocol HTTPClient<RequestOptions>: ~Copyable {
associatedtype RequestOptions: HTTPClientCapability.RequestOptions
associatedtype RequestWriter: AsyncWriter
associatedtype ResponseConcludingReader: ConcludingAsyncReader
var defaultRequestOptions: RequestOptions { get }
func perform<Return: ~Copyable>(
request: HTTPRequest,
body: consuming HTTPClientRequestBody<RequestWriter>?,
options: RequestOptions,
responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return
) async throws -> Return
}
requestusesHTTPRequestwhich comes from the Swift HTTP Types package.bodyis the request body to upload along with the trailer fields. It supports streaming, trailers, and restarting.optionsrepresents the configuration options for performing the HTTP request, including configuration properties and event handlers.responseHandleris a closure where the response can be accessed, including the header, the streamed body, and the trailer.
Request and response body types are shared between the client and the server APIs. More details about the streaming interface can be found here. They are currently based on Span<UInt8> and we are still evaluating the right types to represent the body.
Request body
The request body needs to be restartable since the client retransmits the request upon redirections or authentication challenges, and can optionally be seekable to support resumable uploads. It is represented as a struct with convenience methods to initialize from common body sources such as Data.
public struct HTTPClientRequestBody<Writer>: Sendable
{
public static func restartable(
knownLength: Int64? = nil,
_ body: @escaping @Sendable (consuming Writer) async throws -> HTTPFields?
) -> Self
public static func seekable(
knownLength: Int64? = nil,
_ body: @escaping @Sendable (Int64, consuming Writer) async throws -> HTTPFields?
) -> Self
}
extension HTTPClientRequestBody {
public static func data(_ data: Data) -> Self
}
Request options
RequestOptions is a protocol that defines configuration options for an HTTP client. The specifics are still under active design.
public enum HTTPClientCapability {
public protocol RequestOptions {
}
}
Additional protocols are defined to refine HTTPRequestOptions to add capabilities to the client, so client backend implementations can model their specific capabilities by conforming to a certain set of protocols.
extension HTTPClientCapability {
public protocol TLSVersionSelection: RequestOptions {
var minimumTLSVersion: TLSVersion { get set }
var maximumTLSVersion: TLSVersion { get set }
}
}
Abstract usage
Users of the abstract client interface can depend on the specific capabilities of the client implementation by putting additional constrains on the request options.
func performMySecureQuery(on client: some HTTPClient<some HTTPClientCapability.TLSVersionSelection>)
Convenience methods
We provide convenience methods on top of the core perform method to simplify its usage, e.g. a convenience get method for sending a GET request.
extension HTTPClient {
public func get(
url: URL,
headerFields: HTTPFields = [:],
options: RequestOptions? = nil,
collectUpTo limit: Int,
) async throws -> (response: HTTPResponse, bodyData: Data)
}
To use the convenience get method:
let (response, data) = try await client.get(url, body: bodyData, collectUpTo: .max)
Concrete implementation
The HTTPClient module exposes a platform-default implementation as a static constant. Currently the default implementation on Apple platforms is based on URLSession, and the default implementation on other platforms is based on AsyncHTTPClient.
enum HTTP {
public static func perform<Client: HTTPClient, Return>(
request: HTTPRequest,
body: consuming HTTPClientRequestBody<Client.RequestWriter>? = nil,
options: Client.RequestOptions? = nil,
on client: Client = DefaultHTTPClient.shared,
responseHandler: (HTTPResponse, consuming Client.ResponseConcludingReader) async throws -> Return,
) async throws -> Return
}
DefaultHTTPClient hosts the concrete HTTP client implementation, allowing control of the connection pooling behavior and storage isolation across requests.
public struct DefaultHTTPClient: HTTPClient, Sendable, ~Copyable {
public static var shared: DefaultHTTPClient { get }
public static func withClient<Return: ~Copyable, E: Error>(
poolConfiguration: HTTPConnectionPoolConfiguration,
body: (DefaultHTTPClient) async throws(E) -> Return
) async throws(E) -> Return
}
To use the platform default implementation, simply call perform or other convenience methods on HTTP:
import HTTPClient
// Using the `perform` method
try await HTTP.perform(request: request) { response, body in
guard response.status == .ok else {
throw MyNetworkingError.badResponse(response)
}
// Process body
}
// Using a proposed convenience `get` method
let (response, data) = try await HTTP.get(url, collectUpTo: .max)
Non-goals
These are features we have chosen not to include in the initial scope, though some may be addressed differently in the future.
Support of non-HTTP URL schemes
URLSession supports non-HTTP URL schemes such as file:// and data:// as well as custom schemes. By contrast, HTTPClient focuses exclusively on https:// and http://.
Rather than expanding HTTPClient to support other schemes, we believe a better approach would be to define a separate URLClient API built on top of HTTPClient. This allows HTTPClient to retain its focus and simplicity.
Replacement for background URLSession
Background URLSession supports file uploads, downloads, as well as AVAsset media downloads, with the system scheduling individual background URLSessionTasks to start at optimal times. Even though it shares some of the API surface with the default URLSession, the behaviors are often different in subtle ways.
The HTTPClient API is designed for bidirectional streaming which is not suitable for file-based background transfers. A potential future direction is to design a manifest-based bulk transfer API that can manage uploads and downloads both in-process and out-of-process.
WebSocket
WebSocket is not planned for the initial version of the HTTPClient API. An alternative for WebSocket on Apple platforms is Network Framework.
Why not URLSession or AsyncHTTPClient?
Both APIs were designed before Swift concurrency, with patterns no longer recommended in latest Swift. URLSession has a delegate queue and a deep object hierarchy, while AsyncHTTPClient relies on NIO EventLoop. We can leave the baggage behind when designing a brand-new API.
Feedback
Please use replies for your feedback on the goals and the overall approach. For detailed API discussion, feel free to open issues and pull requests in the swift-http-api-proposal repo, and please tag any related forum posts with http.
Links
Repository
Networking Vision
HTTPServer API