Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,9 @@ let package = Package(
.target(
name: "HTTPClient",
dependencies: [
"HTTPAPIs",
"AsyncStreaming",
"NetworkTypes",
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "HTTPTypesFoundation", package: "swift-http-types"),
"AHCHTTPClient",
"URLSessionHTTPClient",
.product(name: "AsyncHTTPClient", package: "async-http-client"),
],
swiftSettings: extraSettings
),
Expand All @@ -88,7 +86,7 @@ let package = Package(
swiftSettings: extraSettings
),
.target(
name: "AsyncHTTPClientConformance",
name: "AHCHTTPClient",
dependencies: [
"HTTPAPIs",
"AsyncStreaming",
Expand All @@ -101,6 +99,17 @@ let package = Package(
],
swiftSettings: extraSettings
),
.target(
name: "URLSessionHTTPClient",
dependencies: [
"HTTPAPIs",
"AsyncStreaming",
"NetworkTypes",
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "HTTPTypesFoundation", package: "swift-http-types"),
],
swiftSettings: extraSettings
),

// MARK: Conformance Testing

Expand Down Expand Up @@ -164,7 +173,7 @@ let package = Package(
.testTarget(
name: "AsyncHTTPClientConformanceTests",
dependencies: [
"AsyncHTTPClientConformance",
"AHCHTTPClient",
"HTTPClientConformance",
]
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@_spi(ExperimentalHTTPAPIsSupport) public import AsyncHTTPClient
import BasicContainers
import Foundation
public import HTTPAPIs
@_exported public import HTTPAPIs
import HTTPTypes
import NIOCore
import NIOHTTP1
Expand Down
100 changes: 42 additions & 58 deletions Sources/HTTPClient/DefaultHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,40 @@

#if canImport(Darwin) || os(Linux)

/// Configuration options for an HTTP connection pool.
#if canImport(Darwin)
import URLSessionHTTPClient

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct HTTPConnectionPoolConfiguration: Hashable, Sendable {
/// The maximum number of concurrent HTTP/1.1 connections allowed per host.
///
/// This limit helps prevent overwhelming a single host with too many simultaneous
/// connections. HTTP/2 and HTTP/3 connections typically use multiplexing and are
/// not subject to this limit.
///
/// The default value is `6`.
public var maximumConcurrentHTTP1ConnectionsPerHost: Int = 6
typealias ActualHTTPClient = URLSessionHTTPClient
#else
import AsyncHTTPClient
import AHCHTTPClient

public init() {}
}
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
typealias ActualHTTPClient = AsyncHTTPClient.HTTPClient
#endif

/// The default HTTP client that manages persistent connections to HTTP servers.
///
/// `DefaultHTTPClient` provides an efficient HTTP client implementation that reuses
/// connections across multiple requests. It supports HTTP/1.1, HTTP/2, and HTTP/3 protocols,
/// automatically handling connection management, protocol negotiation, and resource cleanup.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct DefaultHTTPClient: HTTPClient, ~Copyable {
public struct DefaultHTTPClient: HTTPAPIs.HTTPClient, ~Copyable {
public struct RequestWriter: AsyncWriter, ~Copyable {
public mutating func write<Result, Failure>(
_ body: (inout OutputSpan<UInt8>) async throws(Failure) -> Result
) async throws(AsyncStreaming.EitherError<any Error, Failure>) -> Result where Failure: Error {
#if canImport(Darwin)
try await self.actual.write(body)
#else
fatalError()
#endif
}

public mutating func write(
_ span: Span<UInt8>
) async throws(EitherError<any Error, AsyncWriterWroteShortError>) {
#if canImport(Darwin)
try await self.actual.write(span)
#else
fatalError()
#endif
}

#if canImport(Darwin)
let actual: URLSessionHTTPClient.RequestWriter
#endif
var actual: ActualHTTPClient.RequestWriter
}

public struct ResponseConcludingReader: ConcludingAsyncReader, ~Copyable {
Expand All @@ -72,42 +60,26 @@ public struct DefaultHTTPClient: HTTPClient, ~Copyable {
maximumCount: Int?,
body: (consuming Span<UInt8>) async throws(Failure) -> Return
) async throws(AsyncStreaming.EitherError<any Error, Failure>) -> Return where Failure: Error {
#if canImport(Darwin)
try await self.actual.read(maximumCount: maximumCount, body: body)
#else
fatalError()
#endif
}

#if canImport(Darwin)
let actual: URLSessionHTTPClient.ResponseConcludingReader.Underlying
#endif
var actual: ActualHTTPClient.ResponseConcludingReader.Underlying
}

public func consumeAndConclude<Return, Failure>(
body: (consuming sending Underlying) async throws(Failure) -> Return
) async throws(Failure) -> (Return, HTTPFields?) where Failure: Error {
#if canImport(Darwin)
try await self.actual.consumeAndConclude { actual throws(Failure) in
try await body(Underlying(actual: actual))
}
#else
fatalError()
#endif
}

#if canImport(Darwin)
let actual: URLSessionHTTPClient.ResponseConcludingReader
#endif
let actual: ActualHTTPClient.ResponseConcludingReader
}

/// A shared connection pool instance with default configuration.
public static var shared: DefaultHTTPClient {
#if canImport(Darwin)
DefaultHTTPClient(client: URLSessionHTTPClient.shared)
#else
fatalError()
#endif
DefaultHTTPClient(client: ActualHTTPClient.shared)
}

/// Creates a client with custom pool configuration and executes a closure with it.
Expand All @@ -126,42 +98,54 @@ public struct DefaultHTTPClient: HTTPClient, ~Copyable {
body: (borrowing DefaultHTTPClient) async throws(Failure) -> Return
) async throws(Failure) -> Return {
#if canImport(Darwin)
try await URLSessionHTTPClient.withClient(poolConfiguration: poolConfiguration) { client throws(Failure) in
var configuration = URLSessionConnectionPoolConfiguration()
configuration.maximumConcurrentHTTP1ConnectionsPerHost = poolConfiguration.maximumConcurrentHTTP1ConnectionsPerHost
return try await URLSessionHTTPClient.withClient(poolConfiguration: configuration) { client throws(Failure) in
try await body(DefaultHTTPClient(client: client))
}
#else
fatalError()
var result: Result<Return, Failure>? = nil
do {
var configuration = AsyncHTTPClient.HTTPClient.Configuration()
configuration.connectionPool.concurrentHTTP1ConnectionsPerHostSoftLimit = poolConfiguration.maximumConcurrentHTTP1ConnectionsPerHost
try await AsyncHTTPClient.HTTPClient.withHTTPClient(configuration: configuration) { client in
do throws(Failure) {
result = .success(try await body(DefaultHTTPClient(client: client)))
} catch {
result = .failure(error)
}
}
} catch {
// Ignore error
}
return try result!.get()
#endif
}

#if canImport(Darwin)
private let client: URLSessionHTTPClient
private let client: ActualHTTPClient

private init(client: URLSessionHTTPClient) {
private init(client: ActualHTTPClient) {
self.client = client
}
#endif

public var defaultRequestOptions: HTTPRequestOptions {
.init()
}

public func perform<Return: ~Copyable>(
request: HTTPRequest,
body: consuming HTTPClientRequestBody<RequestWriter>?,
options: HTTPRequestOptions,
responseHandler: (HTTPResponse, consuming ResponseConcludingReader) async throws -> Return
) async throws -> Return {
#if canImport(Darwin)
// TODO: translate request options
let options = self.client.defaultRequestOptions
let body = body.map {
HTTPClientRequestBody<URLSessionHTTPClient.RequestWriter>(other: $0, transform: RequestWriter.init)
HTTPClientRequestBody<ActualHTTPClient.RequestWriter>(other: $0) { RequestWriter(actual: $0) }
}
return try await self.client.perform(request: request, body: body, options: options) { response, body in
try await responseHandler(response, ResponseConcludingReader(actual: body))
}
#else
fatalError()
#endif
}

public var defaultRequestOptions: HTTPRequestOptions {
.init()
}
}

Expand Down
28 changes: 28 additions & 0 deletions Sources/HTTPClient/HTTPConnectionPoolConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP API Proposal open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift HTTP API Proposal project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Configuration options for an HTTP connection pool.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct HTTPConnectionPoolConfiguration: Hashable, Sendable {
/// The maximum number of concurrent HTTP/1.1 connections allowed per host.
///
/// This limit helps prevent overwhelming a single host with too many simultaneous
/// connections. HTTP/2 and HTTP/3 connections typically use multiplexing and are
/// not subject to this limit.
///
/// The default value is `6`.
public var maximumConcurrentHTTP1ConnectionsPerHost: Int = 6

public init() {}
}
28 changes: 1 addition & 27 deletions Sources/HTTPClient/HTTPRequestOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,8 @@
//
//===----------------------------------------------------------------------===//

public import NetworkTypes

/// The options for the default HTTP client implementation.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct HTTPRequestOptions: HTTPClientCapability.RedirectionHandler, HTTPClientCapability.TLSVersionSelection,
HTTPClientCapability.DeclarativePathSelection
{
public var redirectionHandler: (any HTTPClientRedirectionHandler)? = nil

#if canImport(Darwin)
public var serverTrustHandler: (any HTTPClientServerTrustHandler)? = nil
public var clientCertificateHandler: (any HTTPClientClientCertificateHandler)? = nil
#else
public var serverTrustPolicy: TrustEvaluationPolicy = .default
#endif

public var minimumTLSVersion: TLSVersion = .v1_2
public var maximumTLSVersion: TLSVersion = .v1_3
public var allowsExpensiveNetworkAccess: Bool = true
public var allowsConstrainedNetworkAccess: Bool = true

public struct HTTPRequestOptions: HTTPClientCapability.RequestOptions {
public init() {}
}

#if canImport(Darwin)
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension HTTPRequestOptions: HTTPClientCapability.TLSSecurityHandler {}
#else
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension HTTPRequestOptions: HTTPClientCapability.DeclarativeTLS {}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP API Proposal open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift HTTP API Proposal project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Configuration options for an HTTP connection pool.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct URLSessionConnectionPoolConfiguration: Hashable, Sendable {
/// The maximum number of concurrent HTTP/1.1 connections allowed per host.
///
/// This limit helps prevent overwhelming a single host with too many simultaneous
/// connections. HTTP/2 and HTTP/3 connections typically use multiplexing and are
/// not subject to this limit.
///
/// The default value is `6`.
public var maximumConcurrentHTTP1ConnectionsPerHost: Int = 6

public init() {}
}
Loading
Loading