Skip to content

Commit e294c8f

Browse files
authored
Accurately apply the connect timeout in async code (#616)
Motivation We should apply the connect timeout to the complete set of connection attempts, rather than the request deadline. This allows users fine-grained control over how long we attempt to connect for. This is also the behaviour of our old-school interface. Modifications - Changed the connect deadline calculation for async/await to match that of the future-based code. - Added a connect timeout test. Result Connect timeouts are properly handled
1 parent f90cda4 commit e294c8f

File tree

4 files changed

+83
-2
lines changed

4 files changed

+83
-2
lines changed

Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ extension HTTPClient {
134134
request: request,
135135
requestOptions: .init(idleReadTimeout: nil),
136136
logger: logger,
137-
connectionDeadline: deadline,
137+
connectionDeadline: .now() + (self.configuration.timeout.connectionCreationTimeout),
138138
preferredEventLoop: eventLoop,
139139
responseContinuation: continuation
140140
)

Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ public class HTTPClient {
604604
eventLoopPreference: eventLoopPreference,
605605
task: task,
606606
redirectHandler: redirectHandler,
607-
connectionDeadline: .now() + (self.configuration.timeout.connect ?? .seconds(10)),
607+
connectionDeadline: .now() + (self.configuration.timeout.connectionCreationTimeout),
608608
requestOptions: .fromClientConfiguration(self.configuration),
609609
delegate: delegate
610610
)

Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ extension AsyncAwaitEndToEndTests {
3838
("testCanceling", testCanceling),
3939
("testDeadline", testDeadline),
4040
("testImmediateDeadline", testImmediateDeadline),
41+
("testConnectTimeout", testConnectTimeout),
4142
("testSelfSignedCertificateIsRejectedWithCorrectErrorIfRequestDeadlineIsExceeded", testSelfSignedCertificateIsRejectedWithCorrectErrorIfRequestDeadlineIsExceeded),
4243
("testInvalidURL", testInvalidURL),
4344
("testRedirectChangesHostHeader", testRedirectChangesHostHeader),

Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,27 @@ private func makeDefaultHTTPClient(
3434
}
3535

3636
final class AsyncAwaitEndToEndTests: XCTestCase {
37+
var clientGroup: EventLoopGroup!
38+
var serverGroup: EventLoopGroup!
39+
40+
override func setUp() {
41+
XCTAssertNil(self.clientGroup)
42+
XCTAssertNil(self.serverGroup)
43+
44+
self.clientGroup = getDefaultEventLoopGroup(numberOfThreads: 1)
45+
self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
46+
}
47+
48+
override func tearDown() {
49+
XCTAssertNotNil(self.clientGroup)
50+
XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully())
51+
self.clientGroup = nil
52+
53+
XCTAssertNotNil(self.serverGroup)
54+
XCTAssertNoThrow(try self.serverGroup.syncShutdownGracefully())
55+
self.serverGroup = nil
56+
}
57+
3758
func testSimpleGet() {
3859
#if compiler(>=5.5.2) && canImport(_Concurrency)
3960
guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { return }
@@ -394,6 +415,65 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
394415
#endif
395416
}
396417

418+
func testConnectTimeout() {
419+
#if compiler(>=5.5.2) && canImport(_Concurrency)
420+
guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { return }
421+
XCTAsyncTest(timeout: 60) {
422+
#if os(Linux)
423+
// 198.51.100.254 is reserved for documentation only and therefore should not accept any TCP connection
424+
let url = "http://198.51.100.254/get"
425+
#else
426+
// on macOS we can use the TCP backlog behaviour when the queue is full to simulate a non reachable server.
427+
// this makes this test a bit more stable if `198.51.100.254` actually responds to connection attempt.
428+
// The backlog behaviour on Linux can not be used to simulate a non-reachable server.
429+
// Linux sends a `SYN/ACK` back even if the `backlog` queue is full as it has two queues.
430+
// The second queue is not limit by `ChannelOptions.backlog` but by `/proc/sys/net/ipv4/tcp_max_syn_backlog`.
431+
432+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
433+
defer {
434+
XCTAssertNoThrow(try group.syncShutdownGracefully())
435+
}
436+
437+
let serverChannel = try await ServerBootstrap(group: self.serverGroup)
438+
.serverChannelOption(ChannelOptions.backlog, value: 1)
439+
.serverChannelOption(ChannelOptions.autoRead, value: false)
440+
.bind(host: "127.0.0.1", port: 0)
441+
.get()
442+
defer {
443+
XCTAssertNoThrow(try serverChannel.close().wait())
444+
}
445+
let port = serverChannel.localAddress!.port!
446+
let firstClientChannel = try ClientBootstrap(group: self.serverGroup)
447+
.connect(host: "127.0.0.1", port: port)
448+
.wait()
449+
defer {
450+
XCTAssertNoThrow(try firstClientChannel.close().wait())
451+
}
452+
let url = "http://localhost:\(port)/get"
453+
#endif
454+
455+
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
456+
configuration: .init(timeout: .init(connect: .milliseconds(100), read: .milliseconds(150))))
457+
458+
defer {
459+
XCTAssertNoThrow(try httpClient.syncShutdown())
460+
}
461+
462+
let request = HTTPClientRequest(url: url)
463+
let start = NIODeadline.now()
464+
await XCTAssertThrowsError(try await httpClient.execute(request, deadline: .now() + .seconds(30))) {
465+
XCTAssertEqualTypeAndValue($0, HTTPClientError.connectTimeout)
466+
let end = NIODeadline.now()
467+
let duration = end - start
468+
469+
// We give ourselves 10x slack in order to be confident that even on slow machines this assertion passes.
470+
// It's 30x smaller than our other timeout though.
471+
XCTAssertLessThan(duration, .seconds(1))
472+
}
473+
}
474+
#endif
475+
}
476+
397477
func testSelfSignedCertificateIsRejectedWithCorrectErrorIfRequestDeadlineIsExceeded() {
398478
#if compiler(>=5.5.2) && canImport(_Concurrency)
399479
guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { return }

0 commit comments

Comments
 (0)