From fd530f7c641940a56b6e466a54ee434427951774 Mon Sep 17 00:00:00 2001 From: Guoye Zhang Date: Thu, 12 Feb 2026 15:40:21 -0800 Subject: [PATCH 1/3] Create the default RequestOptions from the client instead of using a required init --- .../HTTPAPIs/Client/HTTPClient+Conveniences.swift | 12 ++++++------ Sources/HTTPAPIs/Client/HTTPClient.swift | 5 ++++- .../Client/HTTPClientCapability+RequestOptions.swift | 1 - Sources/HTTPClient/DefaultHTTPClient.swift | 6 +++++- Sources/HTTPClient/HTTP+Conveniences.swift | 12 ++++++------ Sources/HTTPClient/HTTPRequestOptions.swift | 2 -- .../HTTPClient/URLSession/URLSessionHTTPClient.swift | 7 ++++++- .../HTTPClientConformance.swift | 4 ++-- .../Helpers/HTTPClientAndServerTests.swift | 6 +++++- 9 files changed, 34 insertions(+), 21 deletions(-) diff --git a/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift b/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift index bb6aee0..26becf1 100644 --- a/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift +++ b/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift @@ -40,7 +40,7 @@ extension HTTPClient where Self: ~Copyable { public func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody? = nil, - options: RequestOptions = .init(), + options: RequestOptions? = nil, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return, ) async throws -> Return { return try await self.perform(request: request, body: body, options: options, responseHandler: responseHandler) @@ -63,7 +63,7 @@ extension HTTPClient where Self: ~Copyable { public func get( url: URL, headerFields: HTTPFields = [:], - options: RequestOptions = .init(), + options: RequestOptions? = nil, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(url: url, headerFields: headerFields) @@ -94,7 +94,7 @@ extension HTTPClient where Self: ~Copyable { url: URL, headerFields: HTTPFields = [:], bodyData: Data, - options: RequestOptions = .init(), + options: RequestOptions? = nil, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(method: .post, url: url, headerFields: headerFields) @@ -125,7 +125,7 @@ extension HTTPClient where Self: ~Copyable { url: URL, headerFields: HTTPFields = [:], bodyData: Data, - options: RequestOptions = .init(), + options: RequestOptions? = nil, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(method: .put, url: url, headerFields: headerFields) @@ -156,7 +156,7 @@ extension HTTPClient where Self: ~Copyable { url: URL, headerFields: HTTPFields = [:], bodyData: Data? = nil, - options: RequestOptions = .init(), + options: RequestOptions? = nil, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(method: .delete, url: url, headerFields: headerFields) @@ -187,7 +187,7 @@ extension HTTPClient where Self: ~Copyable { url: URL, headerFields: HTTPFields = [:], bodyData: Data, - options: RequestOptions = .init(), + options: RequestOptions? = nil, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(method: .patch, url: url, headerFields: headerFields) diff --git a/Sources/HTTPAPIs/Client/HTTPClient.swift b/Sources/HTTPAPIs/Client/HTTPClient.swift index 12dc1a6..6f4e0c3 100644 --- a/Sources/HTTPAPIs/Client/HTTPClient.swift +++ b/Sources/HTTPAPIs/Client/HTTPClient.swift @@ -54,7 +54,10 @@ public protocol HTTPClient: ~Copyable { func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?, - options: RequestOptions, + options: RequestOptions?, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return ) async throws -> Return + + /// The default request options for `perform`. + var defaultRequestOptions: RequestOptions { get } } diff --git a/Sources/HTTPAPIs/Client/HTTPClientCapability+RequestOptions.swift b/Sources/HTTPAPIs/Client/HTTPClientCapability+RequestOptions.swift index 93ad5ed..925856a 100644 --- a/Sources/HTTPAPIs/Client/HTTPClientCapability+RequestOptions.swift +++ b/Sources/HTTPAPIs/Client/HTTPClientCapability+RequestOptions.swift @@ -20,6 +20,5 @@ public enum HTTPClientCapability { /// Additional options supported by a subset of clients are defined in child /// protocols to allow libraries to depend on a specific capabilities. public protocol RequestOptions { - init() } } diff --git a/Sources/HTTPClient/DefaultHTTPClient.swift b/Sources/HTTPClient/DefaultHTTPClient.swift index a2ab7e7..0c60f0b 100644 --- a/Sources/HTTPClient/DefaultHTTPClient.swift +++ b/Sources/HTTPClient/DefaultHTTPClient.swift @@ -145,7 +145,7 @@ public struct DefaultHTTPClient: HTTPClient, Sendable, ~Copyable { public func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?, - options: HTTPRequestOptions, + options: HTTPRequestOptions?, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return ) async throws -> Return { #if canImport(Darwin) @@ -159,6 +159,10 @@ public struct DefaultHTTPClient: HTTPClient, Sendable, ~Copyable { fatalError() #endif } + + public var defaultRequestOptions: HTTPRequestOptions { + .init() + } } #endif diff --git a/Sources/HTTPClient/HTTP+Conveniences.swift b/Sources/HTTPClient/HTTP+Conveniences.swift index 3eea904..743d25c 100644 --- a/Sources/HTTPClient/HTTP+Conveniences.swift +++ b/Sources/HTTPClient/HTTP+Conveniences.swift @@ -41,7 +41,7 @@ extension HTTP { public static func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody? = nil, - options: Client.RequestOptions = .init(), + options: Client.RequestOptions? = nil, on client: borrowing Client = DefaultHTTPClient.shared, responseHandler: (HTTPResponse, consuming Client.ResponseConcludingReader) async throws -> Return, ) async throws -> Return { @@ -67,7 +67,7 @@ extension HTTP { public static func get( url: URL, headerFields: HTTPFields = [:], - options: Client.RequestOptions = .init(), + options: Client.RequestOptions? = nil, on client: borrowing Client = DefaultHTTPClient.shared, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { @@ -95,7 +95,7 @@ extension HTTP { url: URL, headerFields: HTTPFields = [:], bodyData: Data, - options: Client.RequestOptions = .init(), + options: Client.RequestOptions? = nil, on client: borrowing Client = DefaultHTTPClient.shared, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { @@ -123,7 +123,7 @@ extension HTTP { url: URL, headerFields: HTTPFields = [:], bodyData: Data, - options: Client.RequestOptions = .init(), + options: Client.RequestOptions? = nil, on client: borrowing Client = DefaultHTTPClient.shared, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { @@ -151,7 +151,7 @@ extension HTTP { url: URL, headerFields: HTTPFields = [:], bodyData: Data? = nil, - options: Client.RequestOptions = .init(), + options: Client.RequestOptions? = nil, on client: borrowing Client = DefaultHTTPClient.shared, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { @@ -179,7 +179,7 @@ extension HTTP { url: URL, headerFields: HTTPFields = [:], bodyData: Data, - options: Client.RequestOptions = .init(), + options: Client.RequestOptions? = nil, on client: borrowing Client = DefaultHTTPClient.shared, collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { diff --git a/Sources/HTTPClient/HTTPRequestOptions.swift b/Sources/HTTPClient/HTTPRequestOptions.swift index bff71a5..fedb430 100644 --- a/Sources/HTTPClient/HTTPRequestOptions.swift +++ b/Sources/HTTPClient/HTTPRequestOptions.swift @@ -32,8 +32,6 @@ public struct HTTPRequestOptions: HTTPClientCapability.RedirectionHandler, HTTPC public var maximumTLSVersion: TLSVersion = .v1_3 public var allowsExpensiveNetworkAccess: Bool = true public var allowsConstrainedNetworkAccess: Bool = true - - public init() {} } #if canImport(Darwin) diff --git a/Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift b/Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift index 6b6af5e..acf8d8f 100644 --- a/Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift +++ b/Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift @@ -236,12 +236,13 @@ final class URLSessionHTTPClient: HTTPClient, IdleTimerEntryProvider, Sendable { func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?, - options: HTTPRequestOptions, + options: HTTPRequestOptions?, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return ) async throws -> Return { guard request.schemeSupported else { throw HTTPTypeConversionError.unsupportedScheme } + let options = options ?? .init() let request = try self.request(for: request, options: options) let session = self.session(for: options) let task: URLSessionTask @@ -276,5 +277,9 @@ final class URLSessionHTTPClient: HTTPClient, IdleTimerEntryProvider, Sendable { } return try result!.get() } + + var defaultRequestOptions: HTTPRequestOptions { + .init() + } } #endif diff --git a/Sources/HTTPClientConformance/HTTPClientConformance.swift b/Sources/HTTPClientConformance/HTTPClientConformance.swift index bd8f212..2f5990c 100644 --- a/Sources/HTTPClientConformance/HTTPClientConformance.swift +++ b/Sources/HTTPClientConformance/HTTPClientConformance.swift @@ -279,7 +279,7 @@ where Client.RequestOptions: HTTPClientCapability.RedirectionHandler { path: "/308" ) - var options = Client.RequestOptions() + var options = client.defaultRequestOptions options.redirectionHandlerClosure = { response, newRequest in #expect(response.status == .permanentRedirect) return .follow(newRequest) @@ -311,7 +311,7 @@ where Client.RequestOptions: HTTPClientCapability.RedirectionHandler { path: "/301" ) - var options = Client.RequestOptions() + var options = client.defaultRequestOptions options.redirectionHandlerClosure = { response, newRequest in #expect(response.status == .movedPermanently) return .follow(newRequest) diff --git a/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift b/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift index 42d28e0..e41c59b 100644 --- a/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift +++ b/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift @@ -136,7 +136,7 @@ final class TestClientAndServer: HTTPClient, HTTPServer, Sendable { func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?, - options: RequestOptions, + options: RequestOptions?, responseHandler: (HTTPResponse, consuming AsyncChannelConcludingAsyncReader) async throws -> Return ) async throws -> Return { let response = try await withCheckedThrowingContinuation { continuation in @@ -160,6 +160,10 @@ final class TestClientAndServer: HTTPClient, HTTPServer, Sendable { ) } + var defaultRequestOptions: RequestOptions { + .init() + } + func serve( handler: some HTTPServerRequestHandler ) async throws { From 03ab52961c48ebd2d7832f8b91214ca8bbf948ca Mon Sep 17 00:00:00 2001 From: Guoye Zhang Date: Fri, 13 Feb 2026 14:40:44 -0800 Subject: [PATCH 2/3] Feedback --- Sources/HTTPAPIs/Client/HTTPClient.swift | 6 +++--- Sources/HTTPClient/HTTPRequestOptions.swift | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/HTTPAPIs/Client/HTTPClient.swift b/Sources/HTTPAPIs/Client/HTTPClient.swift index 6f4e0c3..1c2c73b 100644 --- a/Sources/HTTPAPIs/Client/HTTPClient.swift +++ b/Sources/HTTPAPIs/Client/HTTPClient.swift @@ -35,6 +35,9 @@ public protocol HTTPClient: ~Copyable { associatedtype ResponseConcludingReader: ConcludingAsyncReader, ~Copyable, SendableMetatype where ResponseConcludingReader.Underlying.ReadElement == UInt8, ResponseConcludingReader.FinalElement == HTTPFields? + /// The default request options for `perform`. + var defaultRequestOptions: RequestOptions { get } + /// Performs an HTTP request and processes the response. /// /// This method executes the HTTP request with the specified options, then invokes @@ -57,7 +60,4 @@ public protocol HTTPClient: ~Copyable { options: RequestOptions?, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return ) async throws -> Return - - /// The default request options for `perform`. - var defaultRequestOptions: RequestOptions { get } } diff --git a/Sources/HTTPClient/HTTPRequestOptions.swift b/Sources/HTTPClient/HTTPRequestOptions.swift index fedb430..bff71a5 100644 --- a/Sources/HTTPClient/HTTPRequestOptions.swift +++ b/Sources/HTTPClient/HTTPRequestOptions.swift @@ -32,6 +32,8 @@ public struct HTTPRequestOptions: HTTPClientCapability.RedirectionHandler, HTTPC public var maximumTLSVersion: TLSVersion = .v1_3 public var allowsExpensiveNetworkAccess: Bool = true public var allowsConstrainedNetworkAccess: Bool = true + + public init() {} } #if canImport(Darwin) From e51274bdd000a84724b79e9c0382ed471b1b0cf5 Mon Sep 17 00:00:00 2001 From: Guoye Zhang Date: Tue, 17 Feb 2026 17:02:35 -0800 Subject: [PATCH 3/3] Options not optional on the protocol requirement --- Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift | 6 ++++++ Sources/HTTPAPIs/Client/HTTPClient.swift | 2 +- Sources/HTTPClient/DefaultHTTPClient.swift | 2 +- Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift | 3 +-- Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift b/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift index 26becf1..f1d6d6a 100644 --- a/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift +++ b/Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift @@ -43,6 +43,7 @@ extension HTTPClient where Self: ~Copyable { options: RequestOptions? = nil, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return, ) async throws -> Return { + let options = options ?? self.defaultRequestOptions return try await self.perform(request: request, body: body, options: options, responseHandler: responseHandler) } @@ -67,6 +68,7 @@ extension HTTPClient where Self: ~Copyable { collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(url: url, headerFields: headerFields) + let options = options ?? self.defaultRequestOptions return try await self.perform(request: request, body: nil, options: options) { response, body in ( response, @@ -98,6 +100,7 @@ extension HTTPClient where Self: ~Copyable { collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(method: .post, url: url, headerFields: headerFields) + let options = options ?? self.defaultRequestOptions return try await self.perform(request: request, body: .data(bodyData), options: options) { response, body in ( response, @@ -129,6 +132,7 @@ extension HTTPClient where Self: ~Copyable { collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(method: .put, url: url, headerFields: headerFields) + let options = options ?? self.defaultRequestOptions return try await self.perform(request: request, body: .data(bodyData), options: options) { response, body in ( response, @@ -160,6 +164,7 @@ extension HTTPClient where Self: ~Copyable { collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(method: .delete, url: url, headerFields: headerFields) + let options = options ?? self.defaultRequestOptions return try await self.perform(request: request, body: bodyData.map { .data($0) }, options: options) { response, body in ( response, @@ -191,6 +196,7 @@ extension HTTPClient where Self: ~Copyable { collectUpTo limit: Int, ) async throws -> (response: HTTPResponse, bodyData: Data) { let request = HTTPRequest(method: .patch, url: url, headerFields: headerFields) + let options = options ?? self.defaultRequestOptions return try await self.perform(request: request, body: .data(bodyData), options: options) { response, body in ( response, diff --git a/Sources/HTTPAPIs/Client/HTTPClient.swift b/Sources/HTTPAPIs/Client/HTTPClient.swift index 1c2c73b..e87eab3 100644 --- a/Sources/HTTPAPIs/Client/HTTPClient.swift +++ b/Sources/HTTPAPIs/Client/HTTPClient.swift @@ -57,7 +57,7 @@ public protocol HTTPClient: ~Copyable { func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?, - options: RequestOptions?, + options: RequestOptions, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return ) async throws -> Return } diff --git a/Sources/HTTPClient/DefaultHTTPClient.swift b/Sources/HTTPClient/DefaultHTTPClient.swift index 0c60f0b..353a681 100644 --- a/Sources/HTTPClient/DefaultHTTPClient.swift +++ b/Sources/HTTPClient/DefaultHTTPClient.swift @@ -145,7 +145,7 @@ public struct DefaultHTTPClient: HTTPClient, Sendable, ~Copyable { public func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?, - options: HTTPRequestOptions?, + options: HTTPRequestOptions, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return ) async throws -> Return { #if canImport(Darwin) diff --git a/Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift b/Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift index acf8d8f..b8d8012 100644 --- a/Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift +++ b/Sources/HTTPClient/URLSession/URLSessionHTTPClient.swift @@ -236,13 +236,12 @@ final class URLSessionHTTPClient: HTTPClient, IdleTimerEntryProvider, Sendable { func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?, - options: HTTPRequestOptions?, + options: HTTPRequestOptions, responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return ) async throws -> Return { guard request.schemeSupported else { throw HTTPTypeConversionError.unsupportedScheme } - let options = options ?? .init() let request = try self.request(for: request, options: options) let session = self.session(for: options) let task: URLSessionTask diff --git a/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift b/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift index e41c59b..b1ada4f 100644 --- a/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift +++ b/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift @@ -136,7 +136,7 @@ final class TestClientAndServer: HTTPClient, HTTPServer, Sendable { func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?, - options: RequestOptions?, + options: RequestOptions, responseHandler: (HTTPResponse, consuming AsyncChannelConcludingAsyncReader) async throws -> Return ) async throws -> Return { let response = try await withCheckedThrowingContinuation { continuation in