mirror of
https://github.com/swift-server/async-http-client.git
synced 2026-05-03 07:32:29 +00:00
Add swift-configuration support (#878)
Users should be able to use swift-configuration to create the http client configuration object Changes - duplicate package.swift to create a separate version for 6.0 and 6.1, since Configuration is 6.2+ - add helper to create http client configuration using ConfigReader
This commit is contained in:
+4
-1
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:6.0
|
||||
// swift-tools-version:6.2
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the AsyncHTTPClient open source project
|
||||
@@ -44,6 +44,7 @@ let package = Package(
|
||||
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
|
||||
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"),
|
||||
.package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
@@ -69,6 +70,7 @@ let package = Package(
|
||||
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
|
||||
.product(name: "Atomics", package: "swift-atomics"),
|
||||
.product(name: "Algorithms", package: "swift-algorithms"),
|
||||
.product(name: "Configuration", package: "swift-configuration"),
|
||||
// Observability support
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "Tracing", package: "swift-distributed-tracing"),
|
||||
@@ -90,6 +92,7 @@ let package = Package(
|
||||
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
|
||||
.product(name: "Atomics", package: "swift-atomics"),
|
||||
.product(name: "Algorithms", package: "swift-algorithms"),
|
||||
.product(name: "Configuration", package: "swift-configuration"),
|
||||
// Observability support
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "InMemoryLogging", package: "swift-log"),
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// swift-tools-version:6.0
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the AsyncHTTPClient open source project
|
||||
//
|
||||
// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let strictConcurrencyDevelopment = false
|
||||
|
||||
let strictConcurrencySettings: [SwiftSetting] = {
|
||||
var initialSettings: [SwiftSetting] = []
|
||||
|
||||
if strictConcurrencyDevelopment {
|
||||
// -warnings-as-errors here is a workaround so that IDE-based development can
|
||||
// get tripped up on -require-explicit-sendable.
|
||||
initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
|
||||
}
|
||||
|
||||
return initialSettings
|
||||
}()
|
||||
|
||||
let package = Package(
|
||||
name: "async-http-client",
|
||||
products: [
|
||||
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.24.0"),
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.7.1"),
|
||||
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
|
||||
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CAsyncHTTPClient",
|
||||
cSettings: [
|
||||
.define("_GNU_SOURCE")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "AsyncHTTPClient",
|
||||
dependencies: [
|
||||
.target(name: "CAsyncHTTPClient"),
|
||||
.product(name: "NIO", package: "swift-nio"),
|
||||
.product(name: "NIOTLS", package: "swift-nio"),
|
||||
.product(name: "NIOCore", package: "swift-nio"),
|
||||
.product(name: "NIOPosix", package: "swift-nio"),
|
||||
.product(name: "NIOHTTP1", package: "swift-nio"),
|
||||
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
|
||||
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
|
||||
.product(name: "NIOSSL", package: "swift-nio-ssl"),
|
||||
.product(name: "NIOHTTPCompression", package: "swift-nio-extras"),
|
||||
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
|
||||
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
|
||||
.product(name: "Atomics", package: "swift-atomics"),
|
||||
.product(name: "Algorithms", package: "swift-algorithms"),
|
||||
// Observability support
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "Tracing", package: "swift-distributed-tracing"),
|
||||
],
|
||||
swiftSettings: strictConcurrencySettings
|
||||
),
|
||||
.testTarget(
|
||||
name: "AsyncHTTPClientTests",
|
||||
dependencies: [
|
||||
.target(name: "AsyncHTTPClient"),
|
||||
.product(name: "NIOTLS", package: "swift-nio"),
|
||||
.product(name: "NIOCore", package: "swift-nio"),
|
||||
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
|
||||
.product(name: "NIOEmbedded", package: "swift-nio"),
|
||||
.product(name: "NIOFoundationCompat", package: "swift-nio"),
|
||||
.product(name: "NIOTestUtils", package: "swift-nio"),
|
||||
.product(name: "NIOSSL", package: "swift-nio-ssl"),
|
||||
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
|
||||
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
|
||||
.product(name: "Atomics", package: "swift-atomics"),
|
||||
.product(name: "Algorithms", package: "swift-algorithms"),
|
||||
// Observability support
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "InMemoryLogging", package: "swift-log"),
|
||||
.product(name: "Tracing", package: "swift-distributed-tracing"),
|
||||
.product(name: "InMemoryTracing", package: "swift-distributed-tracing"),
|
||||
],
|
||||
resources: [
|
||||
.copy("Resources/self_signed_cert.pem"),
|
||||
.copy("Resources/self_signed_key.pem"),
|
||||
.copy("Resources/example.com.cert.pem"),
|
||||
.copy("Resources/example.com.private-key.pem"),
|
||||
],
|
||||
swiftSettings: strictConcurrencySettings
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
|
||||
for target in package.targets {
|
||||
switch target.type {
|
||||
case .regular, .test, .executable:
|
||||
var settings = target.swiftSettings ?? []
|
||||
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
|
||||
settings.append(.enableUpcomingFeature("MemberImportVisibility"))
|
||||
target.swiftSettings = settings
|
||||
case .macro, .plugin, .system, .binary:
|
||||
() // not applicable
|
||||
@unknown default:
|
||||
() // we don't know what to do here, do nothing
|
||||
}
|
||||
}
|
||||
// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
|
||||
@@ -0,0 +1,124 @@
|
||||
// swift-tools-version:6.0
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the AsyncHTTPClient open source project
|
||||
//
|
||||
// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let strictConcurrencyDevelopment = false
|
||||
|
||||
let strictConcurrencySettings: [SwiftSetting] = {
|
||||
var initialSettings: [SwiftSetting] = []
|
||||
|
||||
if strictConcurrencyDevelopment {
|
||||
// -warnings-as-errors here is a workaround so that IDE-based development can
|
||||
// get tripped up on -require-explicit-sendable.
|
||||
initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
|
||||
}
|
||||
|
||||
return initialSettings
|
||||
}()
|
||||
|
||||
let package = Package(
|
||||
name: "async-http-client",
|
||||
products: [
|
||||
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.24.0"),
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.7.1"),
|
||||
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
|
||||
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CAsyncHTTPClient",
|
||||
cSettings: [
|
||||
.define("_GNU_SOURCE")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "AsyncHTTPClient",
|
||||
dependencies: [
|
||||
.target(name: "CAsyncHTTPClient"),
|
||||
.product(name: "NIO", package: "swift-nio"),
|
||||
.product(name: "NIOTLS", package: "swift-nio"),
|
||||
.product(name: "NIOCore", package: "swift-nio"),
|
||||
.product(name: "NIOPosix", package: "swift-nio"),
|
||||
.product(name: "NIOHTTP1", package: "swift-nio"),
|
||||
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
|
||||
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
|
||||
.product(name: "NIOSSL", package: "swift-nio-ssl"),
|
||||
.product(name: "NIOHTTPCompression", package: "swift-nio-extras"),
|
||||
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
|
||||
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
|
||||
.product(name: "Atomics", package: "swift-atomics"),
|
||||
.product(name: "Algorithms", package: "swift-algorithms"),
|
||||
// Observability support
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "Tracing", package: "swift-distributed-tracing"),
|
||||
],
|
||||
swiftSettings: strictConcurrencySettings
|
||||
),
|
||||
.testTarget(
|
||||
name: "AsyncHTTPClientTests",
|
||||
dependencies: [
|
||||
.target(name: "AsyncHTTPClient"),
|
||||
.product(name: "NIOTLS", package: "swift-nio"),
|
||||
.product(name: "NIOCore", package: "swift-nio"),
|
||||
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
|
||||
.product(name: "NIOEmbedded", package: "swift-nio"),
|
||||
.product(name: "NIOFoundationCompat", package: "swift-nio"),
|
||||
.product(name: "NIOTestUtils", package: "swift-nio"),
|
||||
.product(name: "NIOSSL", package: "swift-nio-ssl"),
|
||||
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
|
||||
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
|
||||
.product(name: "Atomics", package: "swift-atomics"),
|
||||
.product(name: "Algorithms", package: "swift-algorithms"),
|
||||
// Observability support
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "InMemoryLogging", package: "swift-log"),
|
||||
.product(name: "Tracing", package: "swift-distributed-tracing"),
|
||||
.product(name: "InMemoryTracing", package: "swift-distributed-tracing"),
|
||||
],
|
||||
resources: [
|
||||
.copy("Resources/self_signed_cert.pem"),
|
||||
.copy("Resources/self_signed_key.pem"),
|
||||
.copy("Resources/example.com.cert.pem"),
|
||||
.copy("Resources/example.com.private-key.pem"),
|
||||
],
|
||||
swiftSettings: strictConcurrencySettings
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
|
||||
for target in package.targets {
|
||||
switch target.type {
|
||||
case .regular, .test, .executable:
|
||||
var settings = target.swiftSettings ?? []
|
||||
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
|
||||
settings.append(.enableUpcomingFeature("MemberImportVisibility"))
|
||||
target.swiftSettings = settings
|
||||
case .macro, .plugin, .system, .binary:
|
||||
() // not applicable
|
||||
@unknown default:
|
||||
() // we don't know what to do here, do nothing
|
||||
}
|
||||
}
|
||||
// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
|
||||
@@ -1265,7 +1265,7 @@ extension HTTPClient.Configuration {
|
||||
|
||||
/// Specifies redirect processing settings.
|
||||
public struct RedirectConfiguration: Sendable {
|
||||
enum Mode {
|
||||
enum Mode: Hashable {
|
||||
/// Redirects are not followed.
|
||||
case disallow
|
||||
/// Redirects are followed with a specified limit.
|
||||
@@ -1340,7 +1340,7 @@ extension HTTPClient.Configuration {
|
||||
}
|
||||
|
||||
public struct HTTPVersion: Sendable, Hashable {
|
||||
enum Configuration {
|
||||
enum Configuration: String {
|
||||
case http1Only
|
||||
case automatic
|
||||
}
|
||||
@@ -1394,6 +1394,9 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
|
||||
case deadlineExceeded
|
||||
case httpEndReceivedAfterHeadWith1xx
|
||||
case shutdownUnsupported
|
||||
case invalidRedirectConfiguration
|
||||
case invalidHTTPVersionConfiguration
|
||||
case invalidDNSOverridesConfiguration
|
||||
case internalStateFailure(file: String, line: UInt)
|
||||
}
|
||||
|
||||
@@ -1480,6 +1483,13 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
|
||||
return "HTTP end received after head with 1xx"
|
||||
case .shutdownUnsupported:
|
||||
return "The global singleton HTTP client cannot be shut down"
|
||||
case .invalidRedirectConfiguration:
|
||||
return "The redirect mode specified in the configuration is not a valid value"
|
||||
case .invalidHTTPVersionConfiguration:
|
||||
return "The HTTP version specified in the configuration is not a valid value"
|
||||
case .invalidDNSOverridesConfiguration:
|
||||
return
|
||||
"The DNS overrides specified in the configuration are not valid. Please specify in the format hostname1:ip1,hostname2:ip2"
|
||||
case .internalStateFailure(let file, let line):
|
||||
return
|
||||
"An internal state failure has occurred (File: \(file), line: \(line)). Please open an issue with a reproducer if possible"
|
||||
@@ -1574,6 +1584,15 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
|
||||
/// - Tasks are not processed fast enough on the existing connections, to process all waiters in time
|
||||
public static let getConnectionFromPoolTimeout = HTTPClientError(code: .getConnectionFromPoolTimeout)
|
||||
|
||||
/// The redirect mode specified in the configuration is not a valid value.
|
||||
public static let invalidRedirectConfiguration = HTTPClientError(code: .invalidRedirectConfiguration)
|
||||
|
||||
/// The http version specified in the configuration is not a valid value.
|
||||
public static let invalidHTTPVersionConfiguration = HTTPClientError(code: .invalidHTTPVersionConfiguration)
|
||||
|
||||
/// The DNS overrides specified in the configuration are not valid.
|
||||
public static let invalidDNSOverridesConfiguration = HTTPClientError(code: .invalidDNSOverridesConfiguration)
|
||||
|
||||
/// A state machine has reached an unsupported state, that wasn't considered when implementing.
|
||||
public static func internalStateFailure(file: String = #fileID, line: UInt = #line) -> HTTPClientError {
|
||||
HTTPClientError(code: .internalStateFailure(file: file, line: line))
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the AsyncHTTPClient open source project
|
||||
//
|
||||
// Copyright (c) 2026 Apple Inc. and the AsyncHTTPClient project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if compiler(>=6.2)
|
||||
import Configuration
|
||||
import NIOCore
|
||||
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
extension HTTPClient.Configuration {
|
||||
/// Initializes HTTPClient configuration from a ConfigReader.
|
||||
///
|
||||
/// ## Configuration keys:
|
||||
/// - `dnsOverrides` (string array, optional): Colon-separated host:IP pairs for DNS overrides (e.g., "localhost:127.0.0.1").
|
||||
/// - `redirect` (scoped): Redirect handling configuration read by ``RedirectConfiguration/init(configReader:)``.
|
||||
/// - `timeout` (scoped): Timeout configuration read by ``Timeout/init(configReader:)``.
|
||||
/// - `connectionPool` (scoped): Connection pool configuration read by ``ConnectionPool/init(configReader:)``.
|
||||
/// - `httpVersion` (string, optional, default: automatic): HTTP version to use ( "automatic" or "http1Only").
|
||||
/// - `maximumUsesPerConnection` (int, optional, default: nil, no limit): Maximum uses per connection.
|
||||
///
|
||||
/// - Throws: `HTTPClientError.invalidRedirectConfiguration` if redirect mode is invalid.
|
||||
/// - Throws: `HTTPClientError.invalidHTTPVersionConfiguration` if httpVersion is specified but invalid.
|
||||
public init(configReader: ConfigReader) throws {
|
||||
self.init()
|
||||
|
||||
// Each entry in the list should be a colon separated pair e.g. localhost:127.0.0.1 or localhost:::1
|
||||
if let dnsOverridesList = configReader.stringArray(forKey: "dnsOverrides") {
|
||||
for entry in dnsOverridesList {
|
||||
guard let separatorIndex = entry.firstIndex(of: ":") else {
|
||||
throw HTTPClientError.invalidDNSOverridesConfiguration
|
||||
}
|
||||
let key = entry.prefix(upTo: separatorIndex)
|
||||
let value = entry.suffix(from: entry.index(after: separatorIndex))
|
||||
if key.isEmpty || value.isEmpty {
|
||||
throw HTTPClientError.invalidDNSOverridesConfiguration
|
||||
}
|
||||
self.dnsOverride[String(key)] = String(value.filter { !$0.isWhitespace })
|
||||
}
|
||||
}
|
||||
|
||||
self.redirectConfiguration = try .init(configReader: configReader.scoped(to: "redirect"))
|
||||
self.timeout = .init(configReader: configReader.scoped(to: "timeout"))
|
||||
self.connectionPool = .init(configReader: configReader.scoped(to: "connectionPool"))
|
||||
if let version = try HTTPVersion(configReader: configReader) {
|
||||
self.httpVersion = version
|
||||
}
|
||||
self.maximumUsesPerConnection = configReader.int(forKey: "maximumUsesPerConnection")
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
extension HTTPClient.Configuration.RedirectConfiguration {
|
||||
/// Initializes redirect configuration from a ConfigReader.
|
||||
///
|
||||
/// ## Configuration keys:
|
||||
/// - `mode` (string, optional, default: "follow"): Redirect handling mode ("follow" or "disallow").
|
||||
/// - `maxRedirects` (int, optional, default: 5): Maximum allowed redirects when mode is "follow".
|
||||
/// - `allowCycles` (bool, optional, default: false): Allow cyclic redirects when mode is "follow".
|
||||
///
|
||||
/// - Throws: `HTTPClientError.invalidRedirectConfiguration` if mode is specified but invalid.
|
||||
public init(configReader: ConfigReader) throws {
|
||||
guard let mode = configReader.string(forKey: "mode") else {
|
||||
// default
|
||||
self = .follow(max: 5, allowCycles: false)
|
||||
return
|
||||
}
|
||||
if mode == "follow" {
|
||||
let maxRedirects = configReader.int(forKey: "maxRedirects", default: 5)
|
||||
let allowCycles = configReader.bool(forKey: "allowCycles", default: false)
|
||||
self = .follow(max: maxRedirects, allowCycles: allowCycles)
|
||||
} else if mode == "disallow" {
|
||||
self = .disallow
|
||||
} else {
|
||||
throw HTTPClientError.invalidRedirectConfiguration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
extension HTTPClient.Configuration.Timeout {
|
||||
/// Initializes timeout configuration from a ConfigReader.
|
||||
///
|
||||
/// ## Configuration keys:
|
||||
/// - `connectionMs` (int, optional, default: 10,000): Connection timeout in milliseconds.
|
||||
/// - `readMs` (int, optional): Read timeout in milliseconds.
|
||||
/// - `writeMs` (int, optional): Write timeout in milliseconds.
|
||||
public init(configReader: ConfigReader) {
|
||||
self.init()
|
||||
self.connect = configReader.int(forKey: "connectionMs").map { TimeAmount.milliseconds(Int64($0)) }
|
||||
self.read = configReader.int(forKey: "readMs").map { TimeAmount.milliseconds(Int64($0)) }
|
||||
self.write = configReader.int(forKey: "writeMs").map { TimeAmount.milliseconds(Int64($0)) }
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
extension HTTPClient.Configuration.ConnectionPool {
|
||||
/// Initializes connection pool configuration from a ConfigReader.
|
||||
///
|
||||
/// ## Configuration keys:
|
||||
/// - `idleTimeoutMs` (int, optional, default: 60,000): Connection idle timeout in milliseconds.
|
||||
/// - `concurrentHTTP1ConnectionsPerHostSoftLimit` (int, optional, default: 8): Soft limit for concurrent HTTP/1.1 connections per host.
|
||||
/// - `retryConnectionEstablishment` (bool, optional, default: true): Retry failed connection establishment.
|
||||
/// - `preWarmedHTTP1ConnectionCount` (int, optional, default: 0): Number of pre-warmed HTTP/1.1 connections per host.
|
||||
public init(configReader: ConfigReader) {
|
||||
self.init()
|
||||
self.idleTimeout = TimeAmount.milliseconds(Int64(configReader.int(forKey: "idleTimeoutMs", default: 60000)))
|
||||
self.concurrentHTTP1ConnectionsPerHostSoftLimit = configReader.int(
|
||||
forKey: "concurrentHTTP1ConnectionsPerHostSoftLimit",
|
||||
default: 8
|
||||
)
|
||||
self.retryConnectionEstablishment = configReader.bool(forKey: "retryConnectionEstablishment", default: true)
|
||||
self.preWarmedHTTP1ConnectionCount = configReader.int(forKey: "preWarmedHTTP1ConnectionCount", default: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
extension HTTPClient.Configuration.HTTPVersion {
|
||||
fileprivate init?(configReader: ConfigReader) throws {
|
||||
guard let rawValue = configReader.string(forKey: "httpVersion") else {
|
||||
// Unspecified is not an error. It's an optional prop.
|
||||
return nil
|
||||
}
|
||||
// Specified but invalid IS an error
|
||||
guard let base = Self.Configuration(rawValue: rawValue) else {
|
||||
throw HTTPClientError.invalidHTTPVersionConfiguration
|
||||
}
|
||||
self = .init(configuration: base)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,279 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the AsyncHTTPClient open source project
|
||||
//
|
||||
// Copyright (c) 2026 Apple Inc. and the AsyncHTTPClient project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if compiler(>=6.2)
|
||||
import Configuration
|
||||
import Foundation
|
||||
import NIOCore
|
||||
import Testing
|
||||
|
||||
@testable import AsyncHTTPClient
|
||||
|
||||
struct HTTPClientConfigurationPropsTests {
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func allPropertiesAreSetFromConfig() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"dnsOverrides": .init(.stringArray(["localhost:127.0.0.1", "example.com:192.168.1.1"]), isSecret: false),
|
||||
"redirect.mode": "follow",
|
||||
"redirect.maxRedirects": 10,
|
||||
"redirect.allowCycles": true,
|
||||
|
||||
"timeout.connectionMs": 5000,
|
||||
"timeout.readMs": 30000,
|
||||
"timeout.writeMs": 15000,
|
||||
|
||||
"connectionPool.idleTimeoutMs": 120_000,
|
||||
"connectionPool.concurrentHTTP1ConnectionsPerHostSoftLimit": 16,
|
||||
"connectionPool.retryConnectionEstablishment": false,
|
||||
"connectionPool.preWarmedHTTP1ConnectionCount": 5,
|
||||
|
||||
"httpVersion": "http1Only",
|
||||
"maximumUsesPerConnection": 100,
|
||||
])
|
||||
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
|
||||
#expect(config.dnsOverride["localhost"] == "127.0.0.1")
|
||||
#expect(config.dnsOverride["example.com"] == "192.168.1.1")
|
||||
|
||||
switch config.redirectConfiguration.mode {
|
||||
case .follow(let max, let allowCycles):
|
||||
#expect(max == 10)
|
||||
#expect(allowCycles)
|
||||
case .disallow:
|
||||
Issue.record("Unexpected value")
|
||||
}
|
||||
|
||||
#expect(config.timeout.connect == .milliseconds(5000))
|
||||
#expect(config.timeout.read == .milliseconds(30000))
|
||||
#expect(config.timeout.write == .milliseconds(15000))
|
||||
|
||||
#expect(config.connectionPool.idleTimeout == .milliseconds(120000))
|
||||
#expect(config.connectionPool.concurrentHTTP1ConnectionsPerHostSoftLimit == 16)
|
||||
#expect(config.connectionPool.retryConnectionEstablishment == false)
|
||||
#expect(config.connectionPool.preWarmedHTTP1ConnectionCount == 5)
|
||||
|
||||
#expect(config.httpVersion == .http1Only)
|
||||
|
||||
#expect(config.maximumUsesPerConnection == 100)
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func defaultsAreUsedWhenConfigIsEmpty() throws {
|
||||
let testProvider = InMemoryProvider(values: [:])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
|
||||
#expect(config.dnsOverride.isEmpty)
|
||||
|
||||
#expect(config.timeout.connect == nil)
|
||||
#expect(config.timeout.read == nil)
|
||||
#expect(config.timeout.write == nil)
|
||||
|
||||
#expect(config.connectionPool.idleTimeout == .seconds(60))
|
||||
#expect(config.connectionPool.concurrentHTTP1ConnectionsPerHostSoftLimit == 8)
|
||||
#expect(config.connectionPool.retryConnectionEstablishment == true)
|
||||
#expect(config.connectionPool.preWarmedHTTP1ConnectionCount == 0)
|
||||
|
||||
#expect(config.httpVersion == .automatic)
|
||||
|
||||
#expect(config.maximumUsesPerConnection == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func redirectConfigurationDisallow() throws {
|
||||
let testProvider = InMemoryProvider(values: ["redirect.mode": "disallow"])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
switch config.redirectConfiguration.mode {
|
||||
case .disallow:
|
||||
break
|
||||
case .follow:
|
||||
Issue.record("Unexpected value")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func redirectConfigurationInvalidModeThrowsError() throws {
|
||||
let testProvider = InMemoryProvider(values: ["redirect.mode": "invalid"])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
#expect(throws: HTTPClientError.invalidRedirectConfiguration) {
|
||||
_ = try HTTPClient.Configuration(configReader: configReader)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func httpVersionAutomatic() throws {
|
||||
let testProvider = InMemoryProvider(values: ["httpVersion": "automatic"])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
|
||||
#expect(config.httpVersion == .automatic)
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func httpVersionInvalidThrowsError() throws {
|
||||
let testProvider = InMemoryProvider(values: ["httpVersion": "http3"])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
#expect(throws: HTTPClientError.invalidHTTPVersionConfiguration) {
|
||||
try HTTPClient.Configuration(configReader: configReader)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func dnsOverridesWithIPv6() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"dnsOverrides": .init(.stringArray(["localhost:::1", "example.com:2001:db8::1"]), isSecret: false)
|
||||
])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
|
||||
#expect(config.dnsOverride["localhost"] == "::1")
|
||||
#expect(config.dnsOverride["example.com"] == "2001:db8::1")
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func dnsOverridesWithInvalidFormat() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"dnsOverrides": .init(.stringArray(["invalidentry", "localhost:127.0.0.1"]), isSecret: false)
|
||||
])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
#expect(throws: HTTPClientError.invalidDNSOverridesConfiguration) {
|
||||
_ = try HTTPClient.Configuration(configReader: configReader)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func dnsOverridesWithBlankValue() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"dnsOverrides": .init(.stringArray(["localhost:"]), isSecret: false)
|
||||
])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
#expect(throws: HTTPClientError.invalidDNSOverridesConfiguration) {
|
||||
_ = try HTTPClient.Configuration(configReader: configReader)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func dnsOverridesWithBlankKey() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"dnsOverrides": .init(.stringArray([":127.0.0.1"]), isSecret: false)
|
||||
])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
#expect(throws: HTTPClientError.invalidDNSOverridesConfiguration) {
|
||||
_ = try HTTPClient.Configuration(configReader: configReader)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func dnsOverridesWithSpaces() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"dnsOverrides": .init(.stringArray(["test.com: 127.0.0.1"]), isSecret: false)
|
||||
])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
#expect(config.dnsOverride == ["test.com": "127.0.0.1"])
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func timeoutConfigurationPartial() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"timeout.connectionMs": 1000,
|
||||
"timeout.readMs": 2000,
|
||||
])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
|
||||
#expect(config.timeout.connect == .milliseconds(1000))
|
||||
#expect(config.timeout.read == .milliseconds(2000))
|
||||
#expect(config.timeout.write == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func connectionPoolConfigurationPartial() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"connectionPool.idleTimeoutMs": 90000,
|
||||
"connectionPool.concurrentHTTP1ConnectionsPerHostSoftLimit": 12,
|
||||
])
|
||||
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
|
||||
#expect(config.connectionPool.idleTimeout == .milliseconds(90000))
|
||||
#expect(config.connectionPool.concurrentHTTP1ConnectionsPerHostSoftLimit == 12)
|
||||
// These should use defaults
|
||||
#expect(config.connectionPool.retryConnectionEstablishment)
|
||||
#expect(config.connectionPool.preWarmedHTTP1ConnectionCount == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func redirectConfigurationWithDefaults() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"redirect.mode": "follow"
|
||||
])
|
||||
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
#expect(config.redirectConfiguration.mode == .follow(max: 5, allowCycles: false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func redirectConfigurationCustomValues() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"redirect.mode": "follow",
|
||||
"redirect.maxRedirects": 3,
|
||||
"redirect.allowCycles": true,
|
||||
])
|
||||
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
|
||||
#expect(config.redirectConfiguration.mode == .follow(max: 3, allowCycles: true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
|
||||
func emptyDnsOverridesArray() throws {
|
||||
let testProvider = InMemoryProvider(values: [
|
||||
"dnsOverrides": "[]"
|
||||
])
|
||||
let configReader = ConfigReader(provider: testProvider)
|
||||
let config = try HTTPClient.Configuration(configReader: configReader)
|
||||
|
||||
#expect(config.dnsOverride.isEmpty)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user