Skip to content
Draft
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
6 changes: 4 additions & 2 deletions Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,13 @@ extension HTTPClient {
}
return try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<HTTPClientResponse, Swift.Error>) -> Void in
var requestOptions = RequestOptions.fromClientConfiguration(self.configuration)
requestOptions.apply(stallTimeout: request.stallTimeout)
let transaction = Transaction(
request: request,
requestOptions: .fromClientConfiguration(self.configuration),
requestOptions: requestOptions,
logger: logger,
connectionDeadline: .now() + (self.configuration.timeout.connectionCreationTimeout),
connectionDeadline: .now() + requestOptions.connectionCreationTimeout,
preferredEventLoop: eventLoop,
responseContinuation: continuation
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ extension HTTPClientRequest {
var head: HTTPRequestHead
var body: Body?
var tlsConfiguration: TLSConfiguration?
var stallTimeout: TimeAmount?
}
}

Expand Down Expand Up @@ -92,7 +93,8 @@ extension HTTPClientRequest.Prepared {
headers: headers
),
body: request.body.map { .init($0) },
tlsConfiguration: request.tlsConfiguration
tlsConfiguration: request.tlsConfiguration,
stallTimeout: request.stallTimeout
)
}
}
Expand Down Expand Up @@ -151,6 +153,7 @@ extension HTTPClientRequest {
newRequest.headers = headers
newRequest.body = body
newRequest.localAddress = self.localAddress
newRequest.stallTimeout = self.stallTimeout
return newRequest
}
}
17 changes: 17 additions & 0 deletions Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,30 @@ public struct HTTPClientRequest: Sendable {
/// Defaults to `nil` (use client configuration default).
public var localAddress: String?

/// The maximum duration any single phase of this request may sit without progress
/// before it is considered stalled and aborted.
///
/// When set, overrides ``HTTPClient/Configuration/Timeout-swift.struct/connect``,
/// ``HTTPClient/Configuration/Timeout-swift.struct/read``, and
/// ``HTTPClient/Configuration/Timeout-swift.struct/write`` for this request:
/// - acts as the deadline for acquiring a connection (TCP connect + TLS handshake);
/// - bounds the idle gap between reads from the channel once the request is on the wire;
/// - bounds the idle gap between writes into the channel during body upload.
///
/// The idle timers reset on every byte transferred in their direction, so a slow-but-steady
/// transfer will not trip them; only an actual gap longer than `stallTimeout` will.
/// This is independent of the per-call `deadline`/`timeout`, which bounds the request end-to-end.
/// Defaults to `nil` (use client configuration defaults).
public var stallTimeout: TimeAmount?

public init(url: String) {
self.url = url
self.method = .GET
self.headers = .init()
self.body = .none
self.tlsConfiguration = nil
self.localAddress = nil
self.stallTimeout = nil
}
}

Expand Down
16 changes: 16 additions & 0 deletions Sources/AsyncHTTPClient/ConnectionPool/RequestOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ struct RequestOptions {
var idleReadTimeout: TimeAmount?
/// The maximal `TimeAmount` that is allowed to pass between `write`s into the Channel.
var idleWriteTimeout: TimeAmount?
/// The maximum time the request will wait for a usable connection to be established
/// (TCP connect + TLS handshake) before giving up.
var connectionCreationTimeout: TimeAmount
/// DNS overrides.
var dnsOverride: [String: String]
/// The local IP address to bind outgoing connections to. This is typically used on multi-NIC
Expand All @@ -28,11 +31,13 @@ struct RequestOptions {
init(
idleReadTimeout: TimeAmount?,
idleWriteTimeout: TimeAmount?,
connectionCreationTimeout: TimeAmount,
dnsOverride: [String: String],
localAddress: String? = nil
) {
self.idleReadTimeout = idleReadTimeout
self.idleWriteTimeout = idleWriteTimeout
self.connectionCreationTimeout = connectionCreationTimeout
self.dnsOverride = dnsOverride
self.localAddress = localAddress
}
Expand All @@ -43,8 +48,19 @@ extension RequestOptions {
RequestOptions(
idleReadTimeout: configuration.timeout.read,
idleWriteTimeout: configuration.timeout.write,
connectionCreationTimeout: configuration.timeout.connectionCreationTimeout,
dnsOverride: configuration.dnsOverride,
localAddress: configuration.localAddress
)
}

/// Applies a per-request stall timeout that, when non-nil, replaces the connect, read, and
/// write timeouts derived from the client configuration. A `nil` value is a no-op so the
/// configured defaults stand.
mutating func apply(stallTimeout: TimeAmount?) {
guard let stallTimeout else { return }
self.idleReadTimeout = stallTimeout
self.idleWriteTimeout = stallTimeout
self.connectionCreationTimeout = stallTimeout
}
}
5 changes: 3 additions & 2 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -803,13 +803,14 @@ public final class HTTPClient: Sendable {
)

do {
let requestOptions = RequestOptions.fromClientConfiguration(self.configuration)
let requestBag = try RequestBag(
request: request,
eventLoopPreference: eventLoopPreference,
task: task,
redirectHandler: redirectHandler,
connectionDeadline: .now() + (self.configuration.timeout.connectionCreationTimeout),
requestOptions: .fromClientConfiguration(self.configuration),
connectionDeadline: .now() + requestOptions.connectionCreationTimeout,
requestOptions: requestOptions,
delegate: delegate
)

Expand Down
2 changes: 2 additions & 0 deletions Tests/AsyncHTTPClientTests/RequestBagTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1171,12 +1171,14 @@ extension RequestOptions {
static func forTests(
idleReadTimeout: TimeAmount? = nil,
idleWriteTimeout: TimeAmount? = nil,
connectionCreationTimeout: TimeAmount = .seconds(10),
dnsOverride: [String: String] = [:],
localAddress: String? = nil
) -> Self {
RequestOptions(
idleReadTimeout: idleReadTimeout,
idleWriteTimeout: idleWriteTimeout,
connectionCreationTimeout: connectionCreationTimeout,
dnsOverride: dnsOverride,
localAddress: localAddress
)
Expand Down