From acf576d3cdf98273d333ef072eb5de34eea6a62c Mon Sep 17 00:00:00 2001 From: Guoye Zhang Date: Tue, 7 Apr 2026 10:13:47 -0700 Subject: [PATCH 1/4] Add client evolution proposals --- NNNN-http-client-abstract.md | 324 ++++++++++++++++++++++++++++++++++ NNNN-http-client-concrete.md | 326 +++++++++++++++++++++++++++++++++++ 2 files changed, 650 insertions(+) create mode 100644 NNNN-http-client-abstract.md create mode 100644 NNNN-http-client-concrete.md diff --git a/NNNN-http-client-abstract.md b/NNNN-http-client-abstract.md new file mode 100644 index 0000000..4af00a3 --- /dev/null +++ b/NNNN-http-client-abstract.md @@ -0,0 +1,324 @@ +# Abstract HTTP Client API + +* Proposal: [SE-NNNN](NNNN-swift-http-client.md) +* Authors: [Swift Networking Workgroup](https://github.com/apple/swift-http-api-proposal) +* Review Manager: TBD +* Status: **Awaiting review** +* Vision: [Networking](https://github.com/swiftlang/swift-evolution/blob/main/visions/networking.md) +* Implementation: [apple/swift-http-api-proposal](https://github.com/apple/swift-http-api-proposal) +* Review: ([pitch](https://forums.swift.org/t/designing-an-http-client-api-for-swift/85254)) + +## Summary of changes + +This proposal introduces an abstract HTTP client protocol with capability-based request options. It utilizes modern Swift language features and offers ease-of-use for library authors. A separate proposal covers concrete client implementations. + +## Motivation + +HTTP is the Internet's foundational application-layer protocol, yet the Swift ecosystem lacks a standardized HTTP client API that: +1. Utilizes Swift's modern, evolving language capabilities +2. Operates uniformly across the various platforms that the Swift language supports +3. Offers a dependency injection model that allows libraries to work with different client implementations +4. Supports advanced HTTP features, like bidirectional streaming, trailers, and resumable uploads, with progressive disclosure +5. Enables middleware usage to extend HTTP client functionality + +Other languages, including Rust and Go, typically have a highly popular, if not built-in, HTTP client that works across platforms out-of-the-box, and also utilizes the patterns and capabilities of those languages. + +## Proposed solution + +We propose a new HTTP Client API built on two pieces: + +1. **Abstract protocol interface** (`HTTPClient`) for dependency injection and testability +2. **Convenience methods** for common use cases with progressive disclosure + +### Core protocol + +The `HTTPClient` protocol provides a single `perform` method that handles all HTTP interactions. The request and response metadata are expressed as `HTTPRequest` and `HTTPResponse` types, from the Swift HTTP types package. The protocol requires `Sendable`, ensuring all conforming clients are safe to share across concurrency domains. + +```swift +public protocol HTTPClient: Sendable, ~Copyable, ~Escapable { + associatedtype RequestOptions: HTTPClientCapability.RequestOptions + associatedtype RequestWriter: AsyncWriter, ~Copyable, SendableMetatype + where RequestWriter.WriteElement == UInt8 + associatedtype ResponseConcludingReader: ConcludingAsyncReader, ~Copyable, SendableMetatype + where ResponseConcludingReader.Underlying.ReadElement == UInt8, + ResponseConcludingReader.FinalElement == HTTPFields? + + var defaultRequestOptions: RequestOptions { get } + + mutating func perform( + request: HTTPRequest, + body: consuming HTTPClientRequestBody?, + options: RequestOptions, + responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return + ) async throws -> Return +} +``` + +### Convenience methods for progressive disclosure + +Simple HTTP requests use static methods on the `HTTP` enum. Methods are provided for `get`, `post`, `put`, `delete`, and `patch`, each collecting the response body up to a specified limit. For example, the `get` method signature: + +```swift +public static func get( + url: URL, + headerFields: HTTPFields = [:], + options: Client.RequestOptions? = nil, + on client: borrowing Client = DefaultHTTPClient.shared, + collectUpTo limit: Int +) async throws -> (response: HTTPResponse, bodyData: Data) +``` + +The other methods (`post`, `put`, `delete`, `patch`) follow the same pattern, with `post`, `put`, and `patch` accepting a required `bodyData: Data` parameter and `delete` accepting an optional one. Usage examples: + +```swift +import HTTPClient + +// Simple GET request +let (response, data) = try await HTTP.get(url: url, collectUpTo: .max) + +// POST with a body +let (response, data) = try await HTTP.post( + url: url, + bodyData: jsonData, + collectUpTo: 1024 * 1024 +) + +// DELETE +let (response, data) = try await HTTP.delete(url: url, collectUpTo: .max) + +// Advanced usage with streaming +try await HTTP.perform(request: request) { response, body in + guard response.status == .ok else { + throw MyNetworkingError.badResponse(response) + } + + // Stream the response body + let (_, trailer) = try await body.consumeAndConclude { reader in + try await reader.forEach { span in + print("Received \(span.count) bytes") + } + } + + if let trailer = trailer { + print("Trailer: \(trailer)") + } +} +``` + +### Supplying request bodies and reading response bodies + +Request bodies are supported via an `HTTPClientRequestBody`, which encapsulates a closure responsible for writing the request body, in a way that is either `seekable` or `restartable`. A `restartable` request body supports retries (for redirects and authentication challenges), and a `seekable` request body additionally supports resumable uploads. Trailer fields can also be returned from the closure. + +```swift +public struct HTTPClientRequestBody: Sendable +where Writer.WriteElement == UInt8, Writer: SendableMetatype { + 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 +} +``` + +Responses are delivered via a closure passed into the `responseHandler` parameter of `perform`, which supplies an `HTTPResponse` for HTTP response metadata and a body reader. The return value of the closure is forwarded to the `perform` method. + +### Capability-based request options + +Request options are modeled through capability protocols, allowing clients to advertise supported features. `HTTPClientCapability` is a namespace for these protocols: + +```swift +public enum HTTPClientCapability { + public protocol RequestOptions {} + + public protocol TLSVersionSelection: RequestOptions { + var minimumTLSVersion: TLSVersion { get set } + var maximumTLSVersion: TLSVersion { get set } + } +} +``` + +Whenever possible, options are offered on an individual request basis. + +The abstract API offers the following request options, which may or may not be supported by a particular concrete implementation: +- **TLS Version Selection**: a minimum and maximum TLS version to allow during TLS handshake. + +### Middleware + +A separate `Middleware` module provides a generic, composable protocol for intercepting and transforming values through a chain. The `Middleware` protocol defines a single `intercept(input:next:)` method that receives a value, processes it, and passes a (potentially transformed) value to the next stage. Middleware pipelines can be built declaratively using the `@MiddlewareBuilder` result builder: + +```swift +@MiddlewareBuilder +var pipeline: some Middleware { + LoggingMiddleware() + AuthenticationMiddleware() + RetryMiddleware() +} +``` + +## Detailed design + +### Module structure + +The proposal consists of several interconnected modules, and the abstract API is defined as part of the `HTTPAPIs` module: +- **HTTPAPIs**: Protocol definitions for `HTTPClient` and shared types +- **NetworkTypes**: Currency types defined as needed for request option capabilities + +### `perform` lifecycle + +A call to `perform` proceeds through the following stages: + +1. If a `body` is provided, the implementation invokes its closure, passing a `RequestWriter`. The closure may optionally return trailing `HTTPFields`. +2. The implementation invokes `responseHandler` exactly once, passing an `HTTPResponse` and a `ResponseConcludingReader`. The response handler closure can be invoked concurrently with the request body closure in the case of bidirectional streaming. +3. `perform` returns only after `responseHandler` completes, ensuring the entire request–response cycle is scoped within the call. + +If `responseHandler` throws, the error propagates out of `perform`. + +### Request body + +Request bodies support both retransmission and resumable uploads: + +```swift +// Restartable: can be replayed from the beginning for redirects or retries +let (response, data) = try await HTTP.perform(request: request, body: .restartable { writer in + try await writer.write(bodyBytes) + return nil // no trailer +}) { response, body in + let (data, _) = try await body.collect(upTo: 1024 * 1024) { $0 } + return (response, data) +} + +// Seekable: can resume from an arbitrary offset for resumable uploads +let (response, data) = try await HTTP.perform(request: request, body: .seekable { offset, writer in + try await writer.write(fileBytes[offset...]) + return nil +}) { response, body in + let (data, _) = try await body.collect(upTo: 1024 * 1024) { $0 } + return (response, data) +} +``` + +The closure-based design allows lazy generation of body content. The optional `HTTPFields` return value supports trailers, and the `knownLength` parameter enables the Content-Length header field and progress tracking. + +`HTTPClientRequestBody` is generic over the client's `RequestWriter` associated type. This means request bodies are tied to a specific client type, allowing each concrete implementation to use its own optimized writer without type erasure. + +### Request options and capabilities + +```swift +// A library can require specific capabilities via generic constraints +func fetchMoreSecurely( + using client: borrowing some HTTPClient +) async throws { + var options = client.defaultRequestOptions + options.minimumTLSVersion = .tls13 + try await client.perform(request: request, options: options) { response, body in + // Handle response + } +} +``` + +**Capability pattern benefits:** + +- Clients advertise supported features through protocol conformance +- Library code can require specific capabilities via generic constraints +- Future capabilities can be added without breaking existing clients +- Clear separation between core functionality and optional features + +The protocol's `perform` method takes a non-optional `RequestOptions` parameter. The convenience layer (`HTTP.get`, `HTTP.perform`, etc.) wraps this with an optional `options` parameter that falls back to `client.defaultRequestOptions` when `nil` is passed. This two-layer design keeps the protocol contract explicit while making the common case concise. + +### Middleware + +The `Middleware` protocol and its composition primitives (`ChainedMiddleware`, `@MiddlewareBuilder`) are described in the Proposed solution. This proposal does not define a standardized HTTP middleware contract — the concrete input/output types, response-side interception, and integration with `HTTPClient.perform` are left to a future proposal. + +### Testability + +Because `HTTPClient` is a protocol, libraries and applications can inject mock implementations for testing without depending on a real network stack: + +```swift +struct MockHTTPClient: HTTPClient { + struct RequestOptions: HTTPClientCapability.RequestOptions { + init() {} + } + + var defaultRequestOptions: RequestOptions { .init() } + + func perform( + request: HTTPRequest, + body: consuming HTTPClientRequestBody?, + options: RequestOptions, + responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return + ) async throws -> Return { + // Return a canned response for testing + ... + } +} +``` + +Any code written against a generic `some HTTPClient` parameter can be tested by passing a `MockHTTPClient` instead of a real client, verifying requests and controlling responses without network access. + +## Source compatibility + +This proposal is purely additive and introduces new API surface. It does not modify or deprecate any existing Swift APIs, so there is no impact on source compatibility. + +## ABI compatibility + +This proposal is purely an extension of the Swift ecosystem with new packages and does not modify any existing ABI. + +## Implications on adoption + +The core `HTTPClient` protocol and convenience methods can be back-deployed as a Swift package. Library authors can adopt the `HTTPClient` protocol without coupling to specific implementations, and adding conformance to a type is ABI-additive. The initial release will be marked as pre-1.0 during evolution review. + +## Future directions + +### URLClient abstraction + +While `HTTPClient` focuses exclusively on HTTP/HTTPS, a future `URLClient` protocol could be built on top to support additional URL schemes (file://, data://, custom schemes). This separation keeps `HTTPClient` focused and simple. + +### Background transfer API + +Background URLSession supports system-scheduled uploads, downloads, and media asset downloads. The current streaming-based design is not suitable for file-based background transfers. A future manifest-based bulk transfer API could manage uploads and downloads both in-process and out-of-process, complementing `HTTPClient` for different use cases. + +### WebSocket support + +WebSocket connections upgrade from HTTP but have significantly different semantics. A separate `WebSocketClient` API could be designed in the future, potentially sharing some abstractions with `HTTPClient`. + +### Middleware standardization + +While the repository explores middleware patterns, standardizing middleware protocols for HTTP clients could be addressed in a follow-up proposal, enabling composable request/response transformations. + +## Alternatives considered + +### Extending URLSession + +Rather than creating a new API, we could modernize URLSession with async/await wrappers and streaming support. + +**Advantages:** +- Familiar API for Apple platform developers +- Incremental migration path + +**Disadvantages:** +- URLSession's delegate-based architecture doesn't map well to structured concurrency +- Deep object hierarchies and platform-specific behaviors are hard to abstract +- Supporting non-Apple platforms would require re-implementing URLSession semantics +- Mixing HTTP with other URL schemes complicates the abstraction +- Source stability constraints limit evolution + +### Standardizing AsyncHTTPClient + +We could promote AsyncHTTPClient to be the standard Swift HTTP client across all platforms. + +**Advantages:** +- Proven in production server-side use +- Already cross-platform + +**Disadvantages:** +- EventLoop model doesn't align with structured concurrency +- NIO dependency is heavyweight for client applications +- Apple platform optimizations (URLSession networking stack) would be lost diff --git a/NNNN-http-client-concrete.md b/NNNN-http-client-concrete.md new file mode 100644 index 0000000..d868942 --- /dev/null +++ b/NNNN-http-client-concrete.md @@ -0,0 +1,326 @@ +# Concrete HTTP Client Implementations + +* Proposal: [SE-NNNN](NNNN-swift-http-client-concrete.md) +* Authors: [Swift Networking Workgroup](https://github.com/swiftlang/swift-evolution/blob/main/visions/networking.md) +* Review Manager: TBD +* Status: **Awaiting review** +* Vision: [Networking](https://github.com/swiftlang/swift-evolution/blob/main/visions/networking.md) +* Implementation: [apple/swift-http-api-proposal](https://github.com/apple/swift-http-api-proposal) +* Review: ([pitch](https://forums.swift.org/t/designing-an-http-client-api-for-swift/85254)) + +## Summary of changes + +This proposal introduces concrete HTTP client implementations conforming to the `HTTPClient` protocol defined in SE-NNNN (Abstract HTTP Client API). It provides a `DefaultHTTPClient` that selects the best platform implementation, a URLSession-backed client for Apple platforms, and an AsyncHTTPClient-backed client. + +## Motivation + +The abstract `HTTPClient` protocol from SE-NNNN defines a common interface for HTTP operations, but callers need concrete implementations to actually make requests. The Swift ecosystem currently offers two major HTTP client libraries, URLSession and AsyncHTTPClient, each with platform-specific strengths: + +- **URLSession** integrates deeply with Apple platform networking. +- **AsyncHTTPClient** is cross-platform and proven in server-side production. + +This proposal bridges both to the abstract protocol and provides a `DefaultHTTPClient` that automatically selects the appropriate concrete implementation, so callers who `import HTTPClient` get a working client on every supported platform without choosing an implementation themselves. + +## Proposed solution + +Three concrete clients are introduced: + +1. **`DefaultHTTPClient`**: a platform-selecting wrapper that delegates to the best available concrete implementation. It is the default client used by all `HTTP` convenience methods. +2. **`URLSessionHTTPClient`**: a URLSession-backed implementation available on Apple platforms, exposing URLSession-specific capabilities like TLS version selection, custom redirect handling, and client certificate authentication. +3. **`AHCHTTPClient` module**: an AsyncHTTPClient-backed implementation for non-Apple platforms, exposed as a conformance of the existing `AsyncHTTPClient.HTTPClient` type to the `HTTPAPIs.HTTPClient` protocol. + +### `DefaultHTTPClient` + +Most callers interact with `DefaultHTTPClient` through the `HTTP` static methods, which delegate to `DefaultHTTPClient.shared` by default: + +```swift +import HTTPClient + +// Uses DefaultHTTPClient.shared automatically +let (response, data) = try await HTTP.get(url: url, collectUpTo: .max) +``` + +For advanced use cases, callers can create scoped clients with custom connection pool configuration: + +```swift +try await DefaultHTTPClient.withClient( + poolConfiguration: .init() +) { client in + try await HTTP.perform(request: request, on: client) { response, body in + // Handle response with dedicated connection pool + } +} +``` + +### Platform-specific clients + +When callers need platform-specific capabilities, they can use the concrete implementation clients directly: + +```swift +// URLSession-backed client with TLS version selection and redirect handling +let client = URLSessionHTTPClient.shared +var options = URLSessionRequestOptions() +options.minimumTLSVersion = .v1_3 +options.redirectionHandler = MyRedirectHandler() +``` + +### Request options per implementation + +Each concrete client defines its own `RequestOptions` type conforming to the capability protocols it supports. + +- [ ] Add request options supported by `DefaultHTTPClient` and `AHCClient` when they have some. + +**`URLSessionHTTPClient`** supports the following request options via capability protocols: +- **TLS Version Selection**: a minimum and maximum TLS version to allow during TLS handshake. +- **Redirection Handling**: a custom handler for controlling HTTP redirect behavior. +- **TLS Security Handling**: fine-grained server trust and client certificate authentication via Security framework types. + +It also exposes additional properties not backed by capability protocols: +- **Stall timeout**: maximum duration waiting for new bytes before cancellation. +- **HTTP/3 capability hint**: indicates whether the server is assumed to support HTTP/3. +- **Network access constraints**: controls whether expensive or constrained network access is allowed. + +## Detailed design + +### Module structure + +The concrete implementations are defined across three modules: + +- **HTTPClient**: `DefaultHTTPClient`, `HTTPConnectionPoolConfiguration`, `HTTPRequestOptions`, and the `HTTP` convenience methods (which are generic over any `HTTPClient` but default to `DefaultHTTPClient.shared`, so they are defined here rather than in the abstract `HTTPAPIs` module). This is the primary import for most callers. +- **URLSessionHTTPClient**: `URLSessionHTTPClient`, `URLSessionRequestOptions`, `URLSessionConnectionPoolConfiguration`, and URLSession-specific security types. Available on Apple platforms only. +- **AHCHTTPClient**: Conformance of `AsyncHTTPClient.HTTPClient` to the `HTTPAPIs.HTTPClient` protocol. Available on all platforms where AsyncHTTPClient is supported. + +### `DefaultHTTPClient` + +```swift +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +public final class DefaultHTTPClient: HTTPClient { + public static var shared: DefaultHTTPClient { get } + + public static func withClient( + poolConfiguration: HTTPConnectionPoolConfiguration, + body: (borrowing DefaultHTTPClient) async throws(Failure) -> Return + ) async throws(Failure) -> Return + + public var defaultRequestOptions: HTTPRequestOptions { get } + + public func perform( + request: HTTPRequest, + body: consuming HTTPClientRequestBody?, + options: HTTPRequestOptions, + responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return + ) async throws -> Return +} +``` + +`DefaultHTTPClient` is a final class that wraps the platform-appropriate concrete implementation. On Apple platforms it delegates to `URLSessionHTTPClient`; on other platforms it delegates to `AsyncHTTPClient.HTTPClient`. + +**`shared` vs `withClient`**: The `shared` static property provides a singleton with default connection pool settings, suitable for most use cases. `withClient` creates a scoped client with custom pool configuration; the client is torn down when the closure returns, ensuring connection resources are released. + +#### `HTTPConnectionPoolConfiguration` + +```swift +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +public struct HTTPConnectionPoolConfiguration: Hashable, Sendable { + public var maximumConcurrentHTTP1ConnectionsPerHost: Int // default: 6 + public init() +} +``` + +This configuration controls connection pooling behavior for `DefaultHTTPClient`. It currently supports customizing the HTTP/1.1 connection pool width. + +#### `HTTPRequestOptions` + +```swift +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +public struct HTTPRequestOptions: HTTPClientCapability.RequestOptions { + public init() +} +``` + +`HTTPRequestOptions` is the request options type for `DefaultHTTPClient`. + +### `URLSessionHTTPClient` + +```swift +#if canImport(Darwin) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +public final class URLSessionHTTPClient: HTTPClient, Sendable { + public static var shared: URLSessionHTTPClient { get } + + public static func withClient( + poolConfiguration: URLSessionConnectionPoolConfiguration, + _ body: (URLSessionHTTPClient) async throws(Failure) -> Return + ) async throws(Failure) -> Return + + public var defaultRequestOptions: URLSessionRequestOptions { get } + + public func perform( + request: HTTPRequest, + body: consuming HTTPClientRequestBody?, + options: URLSessionRequestOptions, + responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return + ) async throws -> Return +} +#endif +``` + +`URLSessionHTTPClient` manages URLSession instances and their delegate lifecycle. It conforms to `Sendable` as required by the `HTTPClient` protocol. + +The client internally manages multiple `URLSession` instances, keyed by properties that can only be set on a session configuration rather than per-request (such as TLS version constraints). Sessions are created on demand and reused across requests with matching configurations. + +#### `URLSessionRequestOptions` + +```swift +#if canImport(Darwin) +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +public struct URLSessionRequestOptions: + HTTPClientCapability.RedirectionHandler, + HTTPClientCapability.TLSSecurityHandler, + HTTPClientCapability.TLSVersionSelection +{ + // TLS + public var minimumTLSVersion: TLSVersion // default: .v1_2 + public var maximumTLSVersion: TLSVersion // default: .v1_3 + public var serverTrustHandler: (any HTTPClientServerTrustHandler)? // default: nil + public var clientCertificateHandler: (any HTTPClientClientCertificateHandler)? // default: nil + + // Redirects + public var redirectionHandler: (any HTTPClientRedirectionHandler)? // default: nil + + // Timeouts + public var stallTimeout: Duration? // default: nil + + // URLSession-specific + public var allowsExpensiveNetworkAccess: Bool // default: true + public var allowsConstrainedNetworkAccess: Bool // default: true + public var assumesHTTP3Capable: Bool // default: false + + public init() +} +#endif +``` + +`URLSessionRequestOptions` is the corresponding options type, exposing capabilities specific to Apple's networking stack. + +#### Security types + +**`TrustEvaluationPolicy`** is defined in the `URLSessionHTTPClient` module: + +```swift +public enum TrustEvaluationPolicy: Hashable { + case `default` // System default validation + case allowNameMismatch // Allow certificates without hostname match + case allowAny // Allow any certificate (e.g., self-signed) +} +``` + +**`HTTPClientServerTrustHandler`** and **`HTTPClientClientCertificateHandler`** are Apple-platform-only protocols that provide fine-grained control over TLS authentication challenges using Security framework types (`SecTrust`, `SecIdentity`, `SecCertificate`): + +```swift +#if canImport(Darwin) +public protocol HTTPClientServerTrustHandler: Identifiable, Sendable { + func evaluateServerTrust(_ trust: SecTrust) async throws -> TrustEvaluationResult +} + +public protocol HTTPClientClientCertificateHandler: Identifiable, Sendable { + func handleClientCertificateChallenge( + distinguishedNames: [Data] + ) async throws -> (SecIdentity, [SecCertificate])? +} +#endif +``` + +The `TLSSecurityHandler` capability extends `DeclarativeTLS` (both defined in the `URLSessionHTTPClient` module), providing a bridge: when a `serverTrustHandler` is not set, the `serverTrustPolicy` from `DeclarativeTLS` can be used instead. This means callers can use `TrustEvaluationPolicy` for basic trust control, or the handler protocols for full flexibility. + +#### Redirect handling + +```swift +#if canImport(Darwin) +public protocol HTTPClientRedirectionHandler: Sendable { + func handleRedirection( + response: HTTPResponse, + newRequest: HTTPRequest + ) async throws -> HTTPClientRedirectionAction +} + +public enum HTTPClientRedirectionAction: Sendable { + case follow(HTTPRequest) // Follow the redirect (optionally modified) + case deliverRedirectionResponse // Return the 3xx response to the caller +} +#endif +``` + +When no `redirectionHandler` is set, URLSession follows its default redirect behavior. + +### `AHCHTTPClient` + +```swift +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, *) +extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { + public struct RequestOptions: HTTPClientCapability.RequestOptions { + public init() + } + + public var defaultRequestOptions: RequestOptions { get } + + public func perform( + request: HTTPRequest, + body: consuming HTTPClientRequestBody?, + options: RequestOptions, + responseHandler: (HTTPResponse, consuming ResponseReader) async throws -> Return + ) async throws -> Return +} +``` + +The AHC conformance is implemented as an extension on the existing `AsyncHTTPClient.HTTPClient` type rather than a new wrapper type. This allows callers who already use AsyncHTTPClient to adopt the `HTTPClient` protocol by importing `AHCHTTPClient` — their existing client instance gains the conformance without migration. + +The `RequestOptions` type is currently minimal. AHC's existing configuration (connection pools, timeouts, TLS) is managed through its own initializers and configuration types. Per-request capability protocols will be added as the AHC integration matures. + +### `NetworkTypes` + +The `NetworkTypes` module defines cross-platform currency types used in request options: + +```swift +public struct TLSVersion: Sendable, Hashable { + public static var v1_2: TLSVersion // TLS 1.2 + public static var v1_3: TLSVersion // TLS 1.3 +} +``` + +## Source compatibility + +This proposal is purely additive. It does not modify or deprecate any existing Swift APIs, including URLSession or AsyncHTTPClient. + +## ABI compatibility + +This proposal introduces new packages and does not modify any existing ABI. Platform-specific implementations (URLSession-backed, AsyncHTTPClient-backed) can evolve independently without breaking ABI. + +## Implications on adoption + +- Using `URLSessionHTTPClient` requires targeting Apple platforms +- Using `AHCHTTPClient` requires AsyncHTTPClient as a dependency + +## Future directions + +### Additional capability protocols + +The current set of capability protocols covers TLS configuration, redirects, and server trust. Future capabilities could include: +- Cookie jar management +- Proxy configuration +- HTTP caching policies +- Metrics and progress reporting + +### Cross-platform security APIs + +The `TLSSecurityHandler` capability is currently Apple-platform-only because it depends on Security framework types. The `RedirectionHandler` capability is Apple-platform-only because it is implemented through URLSession's delegate bridging. A future proposal could define cross-platform abstractions for certificate pinning and redirect control that work across all concrete implementations. + +### Connection pool configuration + +The current `HTTPConnectionPoolConfiguration` exposes only HTTP/1.1 connection concurrency. Future additions could include idle connection timeouts, connection-per-host limits for HTTP/2, and DNS resolution configuration. + +## Alternatives considered + +### Not exposing `URLSessionHTTPClient` or `AHCHTTPClient` + +If `DefaultHTTPClient` is the only backend that internally picks URLSession or AHC, users would not be able to pick AHC on Apple platforms. Also URLSession specific options will be harder to support. From 7ca3d4ff743a52f91487caeb8059e4a6090789e0 Mon Sep 17 00:00:00 2001 From: Guoye Zhang Date: Wed, 6 May 2026 17:11:33 -0700 Subject: [PATCH 2/4] Remove concrete and address feedbacks --- NNNN-http-client-abstract.md | 123 +++++-------- NNNN-http-client-concrete.md | 326 ----------------------------------- 2 files changed, 40 insertions(+), 409 deletions(-) delete mode 100644 NNNN-http-client-concrete.md diff --git a/NNNN-http-client-abstract.md b/NNNN-http-client-abstract.md index 4af00a3..935be63 100644 --- a/NNNN-http-client-abstract.md +++ b/NNNN-http-client-abstract.md @@ -21,18 +21,15 @@ HTTP is the Internet's foundational application-layer protocol, yet the Swift ec 4. Supports advanced HTTP features, like bidirectional streaming, trailers, and resumable uploads, with progressive disclosure 5. Enables middleware usage to extend HTTP client functionality -Other languages, including Rust and Go, typically have a highly popular, if not built-in, HTTP client that works across platforms out-of-the-box, and also utilizes the patterns and capabilities of those languages. +Other languages, including Rust and Go, typically have a highly popular, if not built-in, HTTP client that works across platforms out-of-the-box, and also utilizes the patterns and capabilities of those languages. As outlined by [Swift networking vision](https://github.com/swiftlang/swift-evolution/blob/main/visions/networking.md), a cross-platform HTTP API is crucial for Swift competitiveness in networking applications. ## Proposed solution -We propose a new HTTP Client API built on two pieces: - -1. **Abstract protocol interface** (`HTTPClient`) for dependency injection and testability -2. **Convenience methods** for common use cases with progressive disclosure +We propose a new HTTP Client API, starting with the abstract `HTTPClient` protocol in this proposal. Followup proposals about convenience methods and the concrete implementations are based on the protocol defined here. ### Core protocol -The `HTTPClient` protocol provides a single `perform` method that handles all HTTP interactions. The request and response metadata are expressed as `HTTPRequest` and `HTTPResponse` types, from the Swift HTTP types package. The protocol requires `Sendable`, ensuring all conforming clients are safe to share across concurrency domains. +The `HTTPClient` protocol provides a single `perform` method that handles all HTTP interactions. The request and response metadata are expressed as `HTTPRequest` and `HTTPResponse` types, from the Swift HTTP Types package. The protocol requires `Sendable`, ensuring all conforming clients are safe to share across concurrency domains. It also allows ~Copyable and ~Escapable types to conform to the protocol. The `perform` method is mutating, allowing it to mutate state of the client instance. ```swift public protocol HTTPClient: Sendable, ~Copyable, ~Escapable { @@ -54,57 +51,6 @@ public protocol HTTPClient: Sendable, ~Copyable, ~Escapable { } ``` -### Convenience methods for progressive disclosure - -Simple HTTP requests use static methods on the `HTTP` enum. Methods are provided for `get`, `post`, `put`, `delete`, and `patch`, each collecting the response body up to a specified limit. For example, the `get` method signature: - -```swift -public static func get( - url: URL, - headerFields: HTTPFields = [:], - options: Client.RequestOptions? = nil, - on client: borrowing Client = DefaultHTTPClient.shared, - collectUpTo limit: Int -) async throws -> (response: HTTPResponse, bodyData: Data) -``` - -The other methods (`post`, `put`, `delete`, `patch`) follow the same pattern, with `post`, `put`, and `patch` accepting a required `bodyData: Data` parameter and `delete` accepting an optional one. Usage examples: - -```swift -import HTTPClient - -// Simple GET request -let (response, data) = try await HTTP.get(url: url, collectUpTo: .max) - -// POST with a body -let (response, data) = try await HTTP.post( - url: url, - bodyData: jsonData, - collectUpTo: 1024 * 1024 -) - -// DELETE -let (response, data) = try await HTTP.delete(url: url, collectUpTo: .max) - -// Advanced usage with streaming -try await HTTP.perform(request: request) { response, body in - guard response.status == .ok else { - throw MyNetworkingError.badResponse(response) - } - - // Stream the response body - let (_, trailer) = try await body.consumeAndConclude { reader in - try await reader.forEach { span in - print("Received \(span.count) bytes") - } - } - - if let trailer = trailer { - print("Trailer: \(trailer)") - } -} -``` - ### Supplying request bodies and reading response bodies Request bodies are supported via an `HTTPClientRequestBody`, which encapsulates a closure responsible for writing the request body, in a way that is either `seekable` or `restartable`. A `restartable` request body supports retries (for redirects and authentication challenges), and a `seekable` request body additionally supports resumable uploads. Trailer fields can also be returned from the closure. @@ -128,7 +74,20 @@ extension HTTPClientRequestBody { } ``` -Responses are delivered via a closure passed into the `responseHandler` parameter of `perform`, which supplies an `HTTPResponse` for HTTP response metadata and a body reader. The return value of the closure is forwarded to the `perform` method. +Responses are delivered via a closure passed into the `responseHandler` parameter of `perform`, which supplies an `HTTPResponse` for HTTP response metadata and a body reader. The return value of the closure is forwarded to the `perform` method. This ensures that the method is compliant with structured resource management, as all the work of `perform` concludes when the function returns. + +`AsyncReader` and `AsyncWriter` are defined in the Swift Async Algorithms package. `ConcludingAsyncReader` builds on top of `AsyncReader`, allowing a final element which is used for the trailer fields. + +```swift +public protocol ConcludingAsyncReader: ~Copyable, ~Escapable { + associatedtype Underlying: AsyncReader, ~Copyable, ~Escapable + associatedtype FinalElement + + consuming func consumeAndConclude( + body: (consuming sending Underlying) async throws(Failure) -> Return + ) async throws(Failure) -> (Return, FinalElement) +} +``` ### Capability-based request options @@ -137,29 +96,20 @@ Request options are modeled through capability protocols, allowing clients to ad ```swift public enum HTTPClientCapability { public protocol RequestOptions {} - - public protocol TLSVersionSelection: RequestOptions { - var minimumTLSVersion: TLSVersion { get set } - var maximumTLSVersion: TLSVersion { get set } - } } ``` -Whenever possible, options are offered on an individual request basis. +Whenever possible, options are offered on an individual request basis. Options affecting the behaviors of the connection pool are configured on the concrete client implementation itself. The abstract API offers the following request options, which may or may not be supported by a particular concrete implementation: - **TLS Version Selection**: a minimum and maximum TLS version to allow during TLS handshake. -### Middleware - -A separate `Middleware` module provides a generic, composable protocol for intercepting and transforming values through a chain. The `Middleware` protocol defines a single `intercept(input:next:)` method that receives a value, processes it, and passes a (potentially transformed) value to the next stage. Middleware pipelines can be built declaratively using the `@MiddlewareBuilder` result builder: - ```swift -@MiddlewareBuilder -var pipeline: some Middleware { - LoggingMiddleware() - AuthenticationMiddleware() - RetryMiddleware() +extension HTTPClientCapability { + public protocol TLSVersionSelection: RequestOptions { + var minimumTLSVersion: TLSVersion { get set } + var maximumTLSVersion: TLSVersion { get set } + } } ``` @@ -171,13 +121,15 @@ The proposal consists of several interconnected modules, and the abstract API is - **HTTPAPIs**: Protocol definitions for `HTTPClient` and shared types - **NetworkTypes**: Currency types defined as needed for request option capabilities +NetworkTypes module includes common currency types such as IP addresses and TLS versions that are useful outside HTTP. It will become its own separate library. + ### `perform` lifecycle A call to `perform` proceeds through the following stages: 1. If a `body` is provided, the implementation invokes its closure, passing a `RequestWriter`. The closure may optionally return trailing `HTTPFields`. 2. The implementation invokes `responseHandler` exactly once, passing an `HTTPResponse` and a `ResponseConcludingReader`. The response handler closure can be invoked concurrently with the request body closure in the case of bidirectional streaming. -3. `perform` returns only after `responseHandler` completes, ensuring the entire request–response cycle is scoped within the call. +3. `perform` returns only after the request body closure, `responseHandler`, and all other callbacks in `RequestOptions` complete, ensuring the entire request–response cycle is scoped within the call. If `responseHandler` throws, the error propagates out of `perform`. @@ -191,16 +143,16 @@ let (response, data) = try await HTTP.perform(request: request, body: .restartab try await writer.write(bodyBytes) return nil // no trailer }) { response, body in - let (data, _) = try await body.collect(upTo: 1024 * 1024) { $0 } + let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } return (response, data) } // Seekable: can resume from an arbitrary offset for resumable uploads -let (response, data) = try await HTTP.perform(request: request, body: .seekable { offset, writer in +let (response, data) = try await HTTP.perform(request: request, body: .seekable(knownLength: fileBytes.count) { offset, writer in try await writer.write(fileBytes[offset...]) return nil }) { response, body in - let (data, _) = try await body.collect(upTo: 1024 * 1024) { $0 } + let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } return (response, data) } ``` @@ -231,12 +183,6 @@ func fetchMoreSecurely( - Future capabilities can be added without breaking existing clients - Clear separation between core functionality and optional features -The protocol's `perform` method takes a non-optional `RequestOptions` parameter. The convenience layer (`HTTP.get`, `HTTP.perform`, etc.) wraps this with an optional `options` parameter that falls back to `client.defaultRequestOptions` when `nil` is passed. This two-layer design keeps the protocol contract explicit while making the common case concise. - -### Middleware - -The `Middleware` protocol and its composition primitives (`ChainedMiddleware`, `@MiddlewareBuilder`) are described in the Proposed solution. This proposal does not define a standardized HTTP middleware contract — the concrete input/output types, response-side interception, and integration with `HTTPClient.perform` are left to a future proposal. - ### Testability Because `HTTPClient` is a protocol, libraries and applications can inject mock implementations for testing without depending on a real network stack: @@ -291,6 +237,17 @@ WebSocket connections upgrade from HTTP but have significantly different semanti ### Middleware standardization +A separate `Middleware` module provides a generic, composable protocol for intercepting and transforming values through a chain. The `Middleware` protocol defines a single `intercept(input:next:)` method that receives a value, processes it, and passes a (potentially transformed) value to the next stage. Middleware pipelines can be built declaratively using the `@MiddlewareBuilder` result builder: + +```swift +@MiddlewareBuilder +var pipeline: some Middleware { + LoggingMiddleware() + AuthenticationMiddleware() + RetryMiddleware() +} +``` + While the repository explores middleware patterns, standardizing middleware protocols for HTTP clients could be addressed in a follow-up proposal, enabling composable request/response transformations. ## Alternatives considered diff --git a/NNNN-http-client-concrete.md b/NNNN-http-client-concrete.md deleted file mode 100644 index d868942..0000000 --- a/NNNN-http-client-concrete.md +++ /dev/null @@ -1,326 +0,0 @@ -# Concrete HTTP Client Implementations - -* Proposal: [SE-NNNN](NNNN-swift-http-client-concrete.md) -* Authors: [Swift Networking Workgroup](https://github.com/swiftlang/swift-evolution/blob/main/visions/networking.md) -* Review Manager: TBD -* Status: **Awaiting review** -* Vision: [Networking](https://github.com/swiftlang/swift-evolution/blob/main/visions/networking.md) -* Implementation: [apple/swift-http-api-proposal](https://github.com/apple/swift-http-api-proposal) -* Review: ([pitch](https://forums.swift.org/t/designing-an-http-client-api-for-swift/85254)) - -## Summary of changes - -This proposal introduces concrete HTTP client implementations conforming to the `HTTPClient` protocol defined in SE-NNNN (Abstract HTTP Client API). It provides a `DefaultHTTPClient` that selects the best platform implementation, a URLSession-backed client for Apple platforms, and an AsyncHTTPClient-backed client. - -## Motivation - -The abstract `HTTPClient` protocol from SE-NNNN defines a common interface for HTTP operations, but callers need concrete implementations to actually make requests. The Swift ecosystem currently offers two major HTTP client libraries, URLSession and AsyncHTTPClient, each with platform-specific strengths: - -- **URLSession** integrates deeply with Apple platform networking. -- **AsyncHTTPClient** is cross-platform and proven in server-side production. - -This proposal bridges both to the abstract protocol and provides a `DefaultHTTPClient` that automatically selects the appropriate concrete implementation, so callers who `import HTTPClient` get a working client on every supported platform without choosing an implementation themselves. - -## Proposed solution - -Three concrete clients are introduced: - -1. **`DefaultHTTPClient`**: a platform-selecting wrapper that delegates to the best available concrete implementation. It is the default client used by all `HTTP` convenience methods. -2. **`URLSessionHTTPClient`**: a URLSession-backed implementation available on Apple platforms, exposing URLSession-specific capabilities like TLS version selection, custom redirect handling, and client certificate authentication. -3. **`AHCHTTPClient` module**: an AsyncHTTPClient-backed implementation for non-Apple platforms, exposed as a conformance of the existing `AsyncHTTPClient.HTTPClient` type to the `HTTPAPIs.HTTPClient` protocol. - -### `DefaultHTTPClient` - -Most callers interact with `DefaultHTTPClient` through the `HTTP` static methods, which delegate to `DefaultHTTPClient.shared` by default: - -```swift -import HTTPClient - -// Uses DefaultHTTPClient.shared automatically -let (response, data) = try await HTTP.get(url: url, collectUpTo: .max) -``` - -For advanced use cases, callers can create scoped clients with custom connection pool configuration: - -```swift -try await DefaultHTTPClient.withClient( - poolConfiguration: .init() -) { client in - try await HTTP.perform(request: request, on: client) { response, body in - // Handle response with dedicated connection pool - } -} -``` - -### Platform-specific clients - -When callers need platform-specific capabilities, they can use the concrete implementation clients directly: - -```swift -// URLSession-backed client with TLS version selection and redirect handling -let client = URLSessionHTTPClient.shared -var options = URLSessionRequestOptions() -options.minimumTLSVersion = .v1_3 -options.redirectionHandler = MyRedirectHandler() -``` - -### Request options per implementation - -Each concrete client defines its own `RequestOptions` type conforming to the capability protocols it supports. - -- [ ] Add request options supported by `DefaultHTTPClient` and `AHCClient` when they have some. - -**`URLSessionHTTPClient`** supports the following request options via capability protocols: -- **TLS Version Selection**: a minimum and maximum TLS version to allow during TLS handshake. -- **Redirection Handling**: a custom handler for controlling HTTP redirect behavior. -- **TLS Security Handling**: fine-grained server trust and client certificate authentication via Security framework types. - -It also exposes additional properties not backed by capability protocols: -- **Stall timeout**: maximum duration waiting for new bytes before cancellation. -- **HTTP/3 capability hint**: indicates whether the server is assumed to support HTTP/3. -- **Network access constraints**: controls whether expensive or constrained network access is allowed. - -## Detailed design - -### Module structure - -The concrete implementations are defined across three modules: - -- **HTTPClient**: `DefaultHTTPClient`, `HTTPConnectionPoolConfiguration`, `HTTPRequestOptions`, and the `HTTP` convenience methods (which are generic over any `HTTPClient` but default to `DefaultHTTPClient.shared`, so they are defined here rather than in the abstract `HTTPAPIs` module). This is the primary import for most callers. -- **URLSessionHTTPClient**: `URLSessionHTTPClient`, `URLSessionRequestOptions`, `URLSessionConnectionPoolConfiguration`, and URLSession-specific security types. Available on Apple platforms only. -- **AHCHTTPClient**: Conformance of `AsyncHTTPClient.HTTPClient` to the `HTTPAPIs.HTTPClient` protocol. Available on all platforms where AsyncHTTPClient is supported. - -### `DefaultHTTPClient` - -```swift -@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) -public final class DefaultHTTPClient: HTTPClient { - public static var shared: DefaultHTTPClient { get } - - public static func withClient( - poolConfiguration: HTTPConnectionPoolConfiguration, - body: (borrowing DefaultHTTPClient) async throws(Failure) -> Return - ) async throws(Failure) -> Return - - public var defaultRequestOptions: HTTPRequestOptions { get } - - public func perform( - request: HTTPRequest, - body: consuming HTTPClientRequestBody?, - options: HTTPRequestOptions, - responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return - ) async throws -> Return -} -``` - -`DefaultHTTPClient` is a final class that wraps the platform-appropriate concrete implementation. On Apple platforms it delegates to `URLSessionHTTPClient`; on other platforms it delegates to `AsyncHTTPClient.HTTPClient`. - -**`shared` vs `withClient`**: The `shared` static property provides a singleton with default connection pool settings, suitable for most use cases. `withClient` creates a scoped client with custom pool configuration; the client is torn down when the closure returns, ensuring connection resources are released. - -#### `HTTPConnectionPoolConfiguration` - -```swift -@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) -public struct HTTPConnectionPoolConfiguration: Hashable, Sendable { - public var maximumConcurrentHTTP1ConnectionsPerHost: Int // default: 6 - public init() -} -``` - -This configuration controls connection pooling behavior for `DefaultHTTPClient`. It currently supports customizing the HTTP/1.1 connection pool width. - -#### `HTTPRequestOptions` - -```swift -@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) -public struct HTTPRequestOptions: HTTPClientCapability.RequestOptions { - public init() -} -``` - -`HTTPRequestOptions` is the request options type for `DefaultHTTPClient`. - -### `URLSessionHTTPClient` - -```swift -#if canImport(Darwin) -@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) -public final class URLSessionHTTPClient: HTTPClient, Sendable { - public static var shared: URLSessionHTTPClient { get } - - public static func withClient( - poolConfiguration: URLSessionConnectionPoolConfiguration, - _ body: (URLSessionHTTPClient) async throws(Failure) -> Return - ) async throws(Failure) -> Return - - public var defaultRequestOptions: URLSessionRequestOptions { get } - - public func perform( - request: HTTPRequest, - body: consuming HTTPClientRequestBody?, - options: URLSessionRequestOptions, - responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return - ) async throws -> Return -} -#endif -``` - -`URLSessionHTTPClient` manages URLSession instances and their delegate lifecycle. It conforms to `Sendable` as required by the `HTTPClient` protocol. - -The client internally manages multiple `URLSession` instances, keyed by properties that can only be set on a session configuration rather than per-request (such as TLS version constraints). Sessions are created on demand and reused across requests with matching configurations. - -#### `URLSessionRequestOptions` - -```swift -#if canImport(Darwin) -@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) -public struct URLSessionRequestOptions: - HTTPClientCapability.RedirectionHandler, - HTTPClientCapability.TLSSecurityHandler, - HTTPClientCapability.TLSVersionSelection -{ - // TLS - public var minimumTLSVersion: TLSVersion // default: .v1_2 - public var maximumTLSVersion: TLSVersion // default: .v1_3 - public var serverTrustHandler: (any HTTPClientServerTrustHandler)? // default: nil - public var clientCertificateHandler: (any HTTPClientClientCertificateHandler)? // default: nil - - // Redirects - public var redirectionHandler: (any HTTPClientRedirectionHandler)? // default: nil - - // Timeouts - public var stallTimeout: Duration? // default: nil - - // URLSession-specific - public var allowsExpensiveNetworkAccess: Bool // default: true - public var allowsConstrainedNetworkAccess: Bool // default: true - public var assumesHTTP3Capable: Bool // default: false - - public init() -} -#endif -``` - -`URLSessionRequestOptions` is the corresponding options type, exposing capabilities specific to Apple's networking stack. - -#### Security types - -**`TrustEvaluationPolicy`** is defined in the `URLSessionHTTPClient` module: - -```swift -public enum TrustEvaluationPolicy: Hashable { - case `default` // System default validation - case allowNameMismatch // Allow certificates without hostname match - case allowAny // Allow any certificate (e.g., self-signed) -} -``` - -**`HTTPClientServerTrustHandler`** and **`HTTPClientClientCertificateHandler`** are Apple-platform-only protocols that provide fine-grained control over TLS authentication challenges using Security framework types (`SecTrust`, `SecIdentity`, `SecCertificate`): - -```swift -#if canImport(Darwin) -public protocol HTTPClientServerTrustHandler: Identifiable, Sendable { - func evaluateServerTrust(_ trust: SecTrust) async throws -> TrustEvaluationResult -} - -public protocol HTTPClientClientCertificateHandler: Identifiable, Sendable { - func handleClientCertificateChallenge( - distinguishedNames: [Data] - ) async throws -> (SecIdentity, [SecCertificate])? -} -#endif -``` - -The `TLSSecurityHandler` capability extends `DeclarativeTLS` (both defined in the `URLSessionHTTPClient` module), providing a bridge: when a `serverTrustHandler` is not set, the `serverTrustPolicy` from `DeclarativeTLS` can be used instead. This means callers can use `TrustEvaluationPolicy` for basic trust control, or the handler protocols for full flexibility. - -#### Redirect handling - -```swift -#if canImport(Darwin) -public protocol HTTPClientRedirectionHandler: Sendable { - func handleRedirection( - response: HTTPResponse, - newRequest: HTTPRequest - ) async throws -> HTTPClientRedirectionAction -} - -public enum HTTPClientRedirectionAction: Sendable { - case follow(HTTPRequest) // Follow the redirect (optionally modified) - case deliverRedirectionResponse // Return the 3xx response to the caller -} -#endif -``` - -When no `redirectionHandler` is set, URLSession follows its default redirect behavior. - -### `AHCHTTPClient` - -```swift -@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, *) -extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { - public struct RequestOptions: HTTPClientCapability.RequestOptions { - public init() - } - - public var defaultRequestOptions: RequestOptions { get } - - public func perform( - request: HTTPRequest, - body: consuming HTTPClientRequestBody?, - options: RequestOptions, - responseHandler: (HTTPResponse, consuming ResponseReader) async throws -> Return - ) async throws -> Return -} -``` - -The AHC conformance is implemented as an extension on the existing `AsyncHTTPClient.HTTPClient` type rather than a new wrapper type. This allows callers who already use AsyncHTTPClient to adopt the `HTTPClient` protocol by importing `AHCHTTPClient` — their existing client instance gains the conformance without migration. - -The `RequestOptions` type is currently minimal. AHC's existing configuration (connection pools, timeouts, TLS) is managed through its own initializers and configuration types. Per-request capability protocols will be added as the AHC integration matures. - -### `NetworkTypes` - -The `NetworkTypes` module defines cross-platform currency types used in request options: - -```swift -public struct TLSVersion: Sendable, Hashable { - public static var v1_2: TLSVersion // TLS 1.2 - public static var v1_3: TLSVersion // TLS 1.3 -} -``` - -## Source compatibility - -This proposal is purely additive. It does not modify or deprecate any existing Swift APIs, including URLSession or AsyncHTTPClient. - -## ABI compatibility - -This proposal introduces new packages and does not modify any existing ABI. Platform-specific implementations (URLSession-backed, AsyncHTTPClient-backed) can evolve independently without breaking ABI. - -## Implications on adoption - -- Using `URLSessionHTTPClient` requires targeting Apple platforms -- Using `AHCHTTPClient` requires AsyncHTTPClient as a dependency - -## Future directions - -### Additional capability protocols - -The current set of capability protocols covers TLS configuration, redirects, and server trust. Future capabilities could include: -- Cookie jar management -- Proxy configuration -- HTTP caching policies -- Metrics and progress reporting - -### Cross-platform security APIs - -The `TLSSecurityHandler` capability is currently Apple-platform-only because it depends on Security framework types. The `RedirectionHandler` capability is Apple-platform-only because it is implemented through URLSession's delegate bridging. A future proposal could define cross-platform abstractions for certificate pinning and redirect control that work across all concrete implementations. - -### Connection pool configuration - -The current `HTTPConnectionPoolConfiguration` exposes only HTTP/1.1 connection concurrency. Future additions could include idle connection timeouts, connection-per-host limits for HTTP/2, and DNS resolution configuration. - -## Alternatives considered - -### Not exposing `URLSessionHTTPClient` or `AHCHTTPClient` - -If `DefaultHTTPClient` is the only backend that internally picks URLSession or AHC, users would not be able to pick AHC on Apple platforms. Also URLSession specific options will be harder to support. From 913dbb5e9e3c41f545995ecd52d86b9a5730c927 Mon Sep 17 00:00:00 2001 From: Guoye Zhang Date: Wed, 6 May 2026 18:52:09 -0700 Subject: [PATCH 3/4] Refactor proposal --- NNNN-http-client-abstract.md | 224 +++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 103 deletions(-) diff --git a/NNNN-http-client-abstract.md b/NNNN-http-client-abstract.md index 935be63..63505c8 100644 --- a/NNNN-http-client-abstract.md +++ b/NNNN-http-client-abstract.md @@ -25,11 +25,86 @@ Other languages, including Rust and Go, typically have a highly popular, if not ## Proposed solution -We propose a new HTTP Client API, starting with the abstract `HTTPClient` protocol in this proposal. Followup proposals about convenience methods and the concrete implementations are based on the protocol defined here. +We propose a new `HTTPClient` protocol that defines a single `perform` method for all HTTP interactions. This approach is better than the status quo because it: +- Leverages structured concurrency instead of delegate callbacks or completion handlers +- Enables dependency injection so libraries accept any conforming client without coupling to a specific implementation +- Works on every platform Swift supports. + +Followup proposals about convenience methods and concrete implementations build on the protocol defined here. ### Core protocol -The `HTTPClient` protocol provides a single `perform` method that handles all HTTP interactions. The request and response metadata are expressed as `HTTPRequest` and `HTTPResponse` types, from the Swift HTTP Types package. The protocol requires `Sendable`, ensuring all conforming clients are safe to share across concurrency domains. It also allows ~Copyable and ~Escapable types to conform to the protocol. The `perform` method is mutating, allowing it to mutate state of the client instance. +The `HTTPClient` protocol provides a single `perform` method that handles all HTTP interactions. The request and response metadata are expressed as `HTTPRequest` and `HTTPResponse` types from the Swift HTTP Types package. The protocol requires `Sendable`, ensuring all conforming clients are safe to share across concurrency domains. + +Request bodies are written through an `AsyncWriter` and response bodies are read through a `ConcludingAsyncReader` (from the Swift Async Algorithms package), with trailer field support on both sides. + +A simple GET request looks like this: + +```swift +let data = try await client.perform(request: request) { response, body in + let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } + return data +} +``` + +### Supplying request bodies + +Request bodies are provided as closures that write to the client's writer. A `restartable` body supports retries (for redirects and authentication challenges), while a `seekable` body additionally supports resumable uploads: + +```swift +// Restartable: can be replayed from the beginning for redirects or retries +let (response, data) = try await client.perform(request: request, body: .restartable { writer in + try await writer.write(bodyBytes) + return nil // no trailer +}) { response, body in + let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } + return (response, data) +} + +// Seekable: can resume from an arbitrary offset for resumable uploads +let (response, data) = try await client.perform(request: request, body: .seekable(knownLength: fileBytes.count) { offset, writer in + try await writer.write(fileBytes[offset...]) + return nil +}) { response, body in + let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } + return (response, data) +} +``` + +### Capability-based request options + +Request options are modeled through capability protocols, allowing clients to advertise supported features. Library code can require specific capabilities via generic constraints: + +```swift +// A library can require TLS version selection via generic constraints +func fetchMoreSecurely( + using client: borrowing some HTTPClient +) async throws { + var options = client.defaultRequestOptions + options.minimumTLSVersion = .tls13 + try await client.perform(request: request, options: options) { response, body in + // Handle response + } +} +``` + +This pattern allows future capabilities to be added without breaking existing clients, and provides clear separation between core functionality and optional features. + +### Testability + +Because `HTTPClient` is a protocol, libraries and applications can inject mock implementations for testing without depending on a real network stack. Any code written against a generic `some HTTPClient` parameter can be tested by passing a mock client instead, verifying requests and controlling responses without network access. + +## Detailed design + +### Module structure + +The proposal consists of several interconnected modules, and the abstract API is defined as part of the `HTTPAPIs` module: +- **HTTPAPIs**: Protocol definitions for `HTTPClient` and shared types +- **NetworkTypes**: Currency types defined as needed for request option capabilities + +### `HTTPClient` protocol + +The `HTTPClient` protocol is the central abstraction. It allows `~Copyable` and `~Escapable` types to conform, and the `perform` method is mutating, allowing it to mutate state of the client instance. ```swift public protocol HTTPClient: Sendable, ~Copyable, ~Escapable { @@ -49,11 +124,32 @@ public protocol HTTPClient: Sendable, ~Copyable, ~Escapable { responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return ) async throws -> Return } + +extension HTTPClient where Self: ~Copyable & ~Escapable { + public mutating func perform( + request: HTTPRequest, + body: consuming HTTPClientRequestBody? = nil, + options: RequestOptions? = nil, + responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return, + ) async throws -> Return +} ``` -### Supplying request bodies and reading response bodies +The protocol's `perform` method takes a non-optional `RequestOptions` parameter. The convenience `perform` wraps this with an optional `options` parameter that falls back to `client.defaultRequestOptions` when `nil` is passed. -Request bodies are supported via an `HTTPClientRequestBody`, which encapsulates a closure responsible for writing the request body, in a way that is either `seekable` or `restartable`. A `restartable` request body supports retries (for redirects and authentication challenges), and a `seekable` request body additionally supports resumable uploads. Trailer fields can also be returned from the closure. +### `perform` lifecycle + +A call to `perform` proceeds through the following stages: + +1. If a `body` is provided, the implementation invokes its closure, passing a `RequestWriter`. The closure may optionally return trailing `HTTPFields`. +2. The implementation invokes `responseHandler` exactly once, passing an `HTTPResponse` and a `ResponseConcludingReader`. The response handler closure can be invoked concurrently with the request body closure in the case of bidirectional streaming. +3. `perform` returns only after the request body closure, `responseHandler`, and all other callbacks in `RequestOptions` complete, ensuring the entire request–response cycle is scoped within the call. + +If `responseHandler` throws, the error propagates out of `perform`. + +### `HTTPClientRequestBody` + +`HTTPClientRequestBody` encapsulates a closure responsible for writing the request body. It is generic over the client's `RequestWriter` associated type, allowing each concrete implementation to use its own optimized writer without type erasure. ```swift public struct HTTPClientRequestBody: Sendable @@ -74,9 +170,11 @@ extension HTTPClientRequestBody { } ``` -Responses are delivered via a closure passed into the `responseHandler` parameter of `perform`, which supplies an `HTTPResponse` for HTTP response metadata and a body reader. The return value of the closure is forwarded to the `perform` method. This ensures that the method is compliant with structured resource management, as all the work of `perform` concludes when the function returns. +The optional `HTTPFields` return value supports trailers, and the `knownLength` parameter enables the Content-Length header field and progress tracking. + +### `ConcludingAsyncReader` -`AsyncReader` and `AsyncWriter` are defined in the Swift Async Algorithms package. `ConcludingAsyncReader` builds on top of `AsyncReader`, allowing a final element which is used for the trailer fields. +`ConcludingAsyncReader` builds on top of `AsyncReader` (defined in the Swift Async Algorithms package), allowing a final element which is used for the trailer fields. ```swift public protocol ConcludingAsyncReader: ~Copyable, ~Escapable { @@ -91,7 +189,7 @@ public protocol ConcludingAsyncReader: ~Copyable, ~Esc ### Capability-based request options -Request options are modeled through capability protocols, allowing clients to advertise supported features. `HTTPClientCapability` is a namespace for these protocols: +`HTTPClientCapability` is a namespace for capability protocols: ```swift public enum HTTPClientCapability { @@ -102,7 +200,8 @@ public enum HTTPClientCapability { Whenever possible, options are offered on an individual request basis. Options affecting the behaviors of the connection pool are configured on the concrete client implementation itself. The abstract API offers the following request options, which may or may not be supported by a particular concrete implementation: -- **TLS Version Selection**: a minimum and maximum TLS version to allow during TLS handshake. + +#### TLS Version Selection ```swift extension HTTPClientCapability { @@ -113,79 +212,20 @@ extension HTTPClientCapability { } ``` -## Detailed design - -### Module structure - -The proposal consists of several interconnected modules, and the abstract API is defined as part of the `HTTPAPIs` module: -- **HTTPAPIs**: Protocol definitions for `HTTPClient` and shared types -- **NetworkTypes**: Currency types defined as needed for request option capabilities +### `NetworkTypes` NetworkTypes module includes common currency types such as IP addresses and TLS versions that are useful outside HTTP. It will become its own separate library. -### `perform` lifecycle - -A call to `perform` proceeds through the following stages: - -1. If a `body` is provided, the implementation invokes its closure, passing a `RequestWriter`. The closure may optionally return trailing `HTTPFields`. -2. The implementation invokes `responseHandler` exactly once, passing an `HTTPResponse` and a `ResponseConcludingReader`. The response handler closure can be invoked concurrently with the request body closure in the case of bidirectional streaming. -3. `perform` returns only after the request body closure, `responseHandler`, and all other callbacks in `RequestOptions` complete, ensuring the entire request–response cycle is scoped within the call. - -If `responseHandler` throws, the error propagates out of `perform`. - -### Request body - -Request bodies support both retransmission and resumable uploads: - -```swift -// Restartable: can be replayed from the beginning for redirects or retries -let (response, data) = try await HTTP.perform(request: request, body: .restartable { writer in - try await writer.write(bodyBytes) - return nil // no trailer -}) { response, body in - let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } - return (response, data) -} - -// Seekable: can resume from an arbitrary offset for resumable uploads -let (response, data) = try await HTTP.perform(request: request, body: .seekable(knownLength: fileBytes.count) { offset, writer in - try await writer.write(fileBytes[offset...]) - return nil -}) { response, body in - let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } - return (response, data) -} -``` - -The closure-based design allows lazy generation of body content. The optional `HTTPFields` return value supports trailers, and the `knownLength` parameter enables the Content-Length header field and progress tracking. - -`HTTPClientRequestBody` is generic over the client's `RequestWriter` associated type. This means request bodies are tied to a specific client type, allowing each concrete implementation to use its own optimized writer without type erasure. - -### Request options and capabilities - ```swift -// A library can require specific capabilities via generic constraints -func fetchMoreSecurely( - using client: borrowing some HTTPClient -) async throws { - var options = client.defaultRequestOptions - options.minimumTLSVersion = .tls13 - try await client.perform(request: request, options: options) { response, body in - // Handle response - } +public struct TLSVersion: Sendable, Hashable { + public static var v1_2: TLSVersion + public static var v1_3: TLSVersion } ``` -**Capability pattern benefits:** - -- Clients advertise supported features through protocol conformance -- Library code can require specific capabilities via generic constraints -- Future capabilities can be added without breaking existing clients -- Clear separation between core functionality and optional features - ### Testability -Because `HTTPClient` is a protocol, libraries and applications can inject mock implementations for testing without depending on a real network stack: +Because `HTTPClient` is a protocol, libraries and applications can inject mock implementations for testing: ```swift struct MockHTTPClient: HTTPClient { @@ -207,37 +247,35 @@ struct MockHTTPClient: HTTPClient { } ``` -Any code written against a generic `some HTTPClient` parameter can be tested by passing a `MockHTTPClient` instead of a real client, verifying requests and controlling responses without network access. - ## Source compatibility This proposal is purely additive and introduces new API surface. It does not modify or deprecate any existing Swift APIs, so there is no impact on source compatibility. ## ABI compatibility -This proposal is purely an extension of the Swift ecosystem with new packages and does not modify any existing ABI. +This proposal is purely an extension of the Swift ecosystem which can be implemented as a package without any ABI support from the language runtime or standard library. It does not change any existing ABI. ## Implications on adoption -The core `HTTPClient` protocol and convenience methods can be back-deployed as a Swift package. Library authors can adopt the `HTTPClient` protocol without coupling to specific implementations, and adding conformance to a type is ABI-additive. The initial release will be marked as pre-1.0 during evolution review. +The `HTTPClient` protocol is distributed as a Swift package and does not require ABI support from the language runtime. Library authors can adopt the protocol without coupling to specific implementations, and adding conformance to a type is ABI-additive. A library that accepts `some HTTPClient` does not impose any deployment constraints on its users beyond the package version. Adopting the protocol in a library can be un-adopted later without breaking source or ABI compatibility for users of that library. ## Future directions ### URLClient abstraction -While `HTTPClient` focuses exclusively on HTTP/HTTPS, a future `URLClient` protocol could be built on top to support additional URL schemes (file://, data://, custom schemes). This separation keeps `HTTPClient` focused and simple. +`HTTPClient` focuses exclusively on HTTP/HTTPS. A `URLClient` protocol could be built on top to support additional URL schemes (file://, data://, custom schemes), keeping `HTTPClient` focused on its core domain. ### Background transfer API -Background URLSession supports system-scheduled uploads, downloads, and media asset downloads. The current streaming-based design is not suitable for file-based background transfers. A future manifest-based bulk transfer API could manage uploads and downloads both in-process and out-of-process, complementing `HTTPClient` for different use cases. +Background URLSession supports system-scheduled uploads, downloads, and media asset downloads. The current streaming-based design is not suited for file-based background transfers. A manifest-based bulk transfer API could complement `HTTPClient` by managing uploads and downloads both in-process and out-of-process. ### WebSocket support -WebSocket connections upgrade from HTTP but have significantly different semantics. A separate `WebSocketClient` API could be designed in the future, potentially sharing some abstractions with `HTTPClient`. +WebSocket connections upgrade from HTTP but have significantly different semantics. A separate `WebSocketClient` protocol could share some abstractions with `HTTPClient` while providing message-oriented framing. ### Middleware standardization -A separate `Middleware` module provides a generic, composable protocol for intercepting and transforming values through a chain. The `Middleware` protocol defines a single `intercept(input:next:)` method that receives a value, processes it, and passes a (potentially transformed) value to the next stage. Middleware pipelines can be built declaratively using the `@MiddlewareBuilder` result builder: +The repository includes a generic, composable `Middleware` protocol for intercepting and transforming values through a chain. Middleware pipelines can be built declaratively using the `@MiddlewareBuilder` result builder: ```swift @MiddlewareBuilder @@ -248,34 +286,14 @@ var pipeline: some Middleware { } ``` -While the repository explores middleware patterns, standardizing middleware protocols for HTTP clients could be addressed in a follow-up proposal, enabling composable request/response transformations. +Standardizing middleware protocols specifically for HTTP clients could be addressed in a follow-up proposal. ## Alternatives considered ### Extending URLSession -Rather than creating a new API, we could modernize URLSession with async/await wrappers and streaming support. - -**Advantages:** -- Familiar API for Apple platform developers -- Incremental migration path - -**Disadvantages:** -- URLSession's delegate-based architecture doesn't map well to structured concurrency -- Deep object hierarchies and platform-specific behaviors are hard to abstract -- Supporting non-Apple platforms would require re-implementing URLSession semantics -- Mixing HTTP with other URL schemes complicates the abstraction -- Source stability constraints limit evolution +Rather than creating a new protocol, URLSession could be modernized with async/await wrappers and streaming support. This would offer a familiar API for Apple platform developers and an incremental migration path. However, URLSession's delegate-based architecture does not map well to structured concurrency, its deep object hierarchies and platform-specific behaviors are hard to abstract across platforms, and mixing HTTP with other URL schemes complicates the abstraction. Source stability constraints on URLSession also limit how far the API can evolve. ### Standardizing AsyncHTTPClient -We could promote AsyncHTTPClient to be the standard Swift HTTP client across all platforms. - -**Advantages:** -- Proven in production server-side use -- Already cross-platform - -**Disadvantages:** -- EventLoop model doesn't align with structured concurrency -- NIO dependency is heavyweight for client applications -- Apple platform optimizations (URLSession networking stack) would be lost +AsyncHTTPClient could be promoted to the standard Swift HTTP client. It is already proven in production server-side use and cross-platform. However, its EventLoop model does not align with structured concurrency, the SwiftNIO dependency is heavyweight for client applications, and Apple platform optimizations (the URLSession networking stack) would be lost. From 0758169174257492c007f58d8e4a568a6cd81293 Mon Sep 17 00:00:00 2001 From: Guoye Zhang Date: Wed, 6 May 2026 19:01:09 -0700 Subject: [PATCH 4/4] Fix example --- NNNN-http-client-abstract.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NNNN-http-client-abstract.md b/NNNN-http-client-abstract.md index 63505c8..b5c4e27 100644 --- a/NNNN-http-client-abstract.md +++ b/NNNN-http-client-abstract.md @@ -42,7 +42,7 @@ A simple GET request looks like this: ```swift let data = try await client.perform(request: request) { response, body in - let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } + let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { Data(copying: $0) } return data } ``` @@ -57,7 +57,7 @@ let (response, data) = try await client.perform(request: request, body: .restart try await writer.write(bodyBytes) return nil // no trailer }) { response, body in - let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } + let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { Data(copying: $0) } return (response, data) } @@ -66,7 +66,7 @@ let (response, data) = try await client.perform(request: request, body: .seekabl try await writer.write(fileBytes[offset...]) return nil }) { response, body in - let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { $0 } + let (data, trailer) = try await body.collect(upTo: 1024 * 1024) { Data(copying: $0) } return (response, data) } ```