Skip to content

Commit 701c1d4

Browse files
committed
Preserve connection errors for user
1 parent 75b716e commit 701c1d4

File tree

5 files changed

+127
-3
lines changed

5 files changed

+127
-3
lines changed

Diff for: Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1Connections.swift

+5
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ extension HTTPConnectionPool {
257257
return connecting
258258
}
259259

260+
/// Is there at least one connection that is able to run requests
261+
var hasActiveConnections: Bool {
262+
self.connections.contains(where: { $0.isIdle || $0.isLeased })
263+
}
264+
260265
func startingEventLoopConnections(on eventLoop: EventLoop) -> Int {
261266
return self.connections[self.overflowIndex..<self.connections.endIndex].reduce(into: 0) { count, connection in
262267
guard connection.eventLoop === eventLoop else { return }

Diff for: Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ extension HTTPConnectionPool {
2626

2727
private var connections: HTTP1Connections
2828
private var failedConsecutiveConnectionAttempts: Int = 0
29+
/// the error from the last connection creation
30+
private var lastConnectFailure: Error?
2931

3032
private var requests: RequestQueue
3133
private var state: State = .running
@@ -136,12 +138,14 @@ extension HTTPConnectionPool {
136138

137139
mutating func newHTTP1ConnectionEstablished(_ connection: Connection) -> Action {
138140
self.failedConsecutiveConnectionAttempts = 0
141+
self.lastConnectFailure = nil
139142
let (index, context) = self.connections.newHTTP1ConnectionEstablished(connection)
140143
return self.nextActionForIdleConnection(at: index, context: context)
141144
}
142145

143146
mutating func failedToCreateNewConnection(_ error: Error, connectionID: Connection.ID) -> Action {
144147
self.failedConsecutiveConnectionAttempts += 1
148+
self.lastConnectFailure = error
145149

146150
switch self.state {
147151
case .running:
@@ -223,8 +227,14 @@ extension HTTPConnectionPool {
223227
mutating func timeoutRequest(_ requestID: Request.ID) -> Action {
224228
// 1. check requests in queue
225229
if let request = self.requests.remove(requestID) {
230+
var error: Error = HTTPClientError.getConnectionFromPoolTimeout
231+
if let lastError = self.lastConnectFailure {
232+
error = lastError
233+
} else if !self.connections.hasActiveConnections {
234+
error = HTTPClientError.connectTimeout
235+
}
226236
return .init(
227-
request: .failRequest(request, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false),
237+
request: .failRequest(request, error, cancelTimeout: false),
228238
connection: .none
229239
)
230240
}

Diff for: Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP1StateTests+XCTest.swift

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ extension HTTPConnectionPool_HTTP1StateMachineTests {
3838
("testConnectionPoolFullOfParkedConnectionsIsShutdownImmediately", testConnectionPoolFullOfParkedConnectionsIsShutdownImmediately),
3939
("testParkedConnectionTimesOutButIsAlsoClosedByRemote", testParkedConnectionTimesOutButIsAlsoClosedByRemote),
4040
("testConnectionBackoffVsShutdownRace", testConnectionBackoffVsShutdownRace),
41+
("testRequestThatTimesOutIsFailedWithLastConnectionCreationError", testRequestThatTimesOutIsFailedWithLastConnectionCreationError),
42+
("testRequestThatTimesOutBeforeAConnectionIsEstablishedIsFailedWithConnectTimeoutError", testRequestThatTimesOutBeforeAConnectionIsEstablishedIsFailedWithConnectTimeoutError),
43+
("testRequestThatTimesOutAfterAConnectionWasEstablishedSuccessfullyTimesOutWithGenericError", testRequestThatTimesOutAfterAConnectionWasEstablishedSuccessfullyTimesOutWithGenericError),
4144
]
4245
}
4346
}

Diff for: Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP1StateTests.swift

+107-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
153153
return XCTFail("Unexpected request action: \(action.request)")
154154
}
155155
XCTAssert(requestToFail.__testOnly_wrapped_request() === mockRequest) // XCTAssertIdentical not available on Linux
156-
XCTAssertEqual(requestError as? HTTPClientError, .getConnectionFromPoolTimeout)
156+
XCTAssertEqual(requestError as? HTTPClientError, .connectTimeout)
157157
XCTAssertEqual(failRequest.connection, .none)
158158

159159
// 4. retry connection, but no more queued requests.
@@ -626,4 +626,110 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
626626

627627
XCTAssertEqual(state.connectionCreationBackoffDone(connectionID), .none)
628628
}
629+
630+
func testRequestThatTimesOutIsFailedWithLastConnectionCreationError() {
631+
let elg = EmbeddedEventLoopGroup(loops: 1)
632+
defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) }
633+
634+
var state = HTTPConnectionPool.StateMachine(
635+
eventLoopGroup: elg,
636+
idGenerator: .init(),
637+
maximumConcurrentHTTP1Connections: 6
638+
)
639+
640+
let mockRequest = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
641+
let request = HTTPConnectionPool.Request(mockRequest)
642+
643+
let executeAction = state.executeRequest(request)
644+
guard case .createConnection(let connectionID, on: let connEL) = executeAction.connection else {
645+
return XCTFail("Expected to create a connection")
646+
}
647+
648+
XCTAssertEqual(executeAction.request, .scheduleRequestTimeout(for: request, on: mockRequest.eventLoop))
649+
650+
let failAction = state.failedToCreateNewConnection(HTTPClientError.httpProxyHandshakeTimeout, connectionID: connectionID)
651+
guard case .scheduleBackoffTimer(connectionID, backoff: _, on: let timerEL) = failAction.connection else {
652+
return XCTFail("Expected to create a backoff timer")
653+
}
654+
XCTAssert(timerEL === connEL)
655+
XCTAssertEqual(failAction.request, .none)
656+
657+
let timeoutAction = state.timeoutRequest(request.id)
658+
XCTAssertEqual(timeoutAction.request, .failRequest(request, HTTPClientError.httpProxyHandshakeTimeout, cancelTimeout: false))
659+
XCTAssertEqual(timeoutAction.connection, .none)
660+
}
661+
662+
func testRequestThatTimesOutBeforeAConnectionIsEstablishedIsFailedWithConnectTimeoutError() {
663+
let eventLoop = EmbeddedEventLoop()
664+
defer { XCTAssertNoThrow(try eventLoop.syncShutdownGracefully()) }
665+
666+
var state = HTTPConnectionPool.StateMachine(
667+
eventLoopGroup: eventLoop,
668+
idGenerator: .init(),
669+
maximumConcurrentHTTP1Connections: 6
670+
)
671+
672+
let mockRequest = MockHTTPRequest(eventLoop: eventLoop.next(), requiresEventLoopForChannel: false)
673+
let request = HTTPConnectionPool.Request(mockRequest)
674+
675+
let executeAction = state.executeRequest(request)
676+
guard case .createConnection(_, on: _) = executeAction.connection else {
677+
return XCTFail("Expected to create a connection")
678+
}
679+
XCTAssertEqual(executeAction.request, .scheduleRequestTimeout(for: request, on: mockRequest.eventLoop))
680+
681+
let timeoutAction = state.timeoutRequest(request.id)
682+
XCTAssertEqual(timeoutAction.request, .failRequest(request, HTTPClientError.connectTimeout, cancelTimeout: false))
683+
XCTAssertEqual(timeoutAction.connection, .none)
684+
}
685+
686+
func testRequestThatTimesOutAfterAConnectionWasEstablishedSuccessfullyTimesOutWithGenericError() {
687+
let elg = EmbeddedEventLoopGroup(loops: 1)
688+
defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) }
689+
690+
var state = HTTPConnectionPool.StateMachine(
691+
eventLoopGroup: elg,
692+
idGenerator: .init(),
693+
maximumConcurrentHTTP1Connections: 6
694+
)
695+
696+
let mockRequest1 = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
697+
let request1 = HTTPConnectionPool.Request(mockRequest1)
698+
699+
let executeAction1 = state.executeRequest(request1)
700+
guard case .createConnection(let connectionID1, on: let connEL1) = executeAction1.connection else {
701+
return XCTFail("Expected to create a connection")
702+
}
703+
XCTAssert(mockRequest1.eventLoop === connEL1)
704+
705+
XCTAssertEqual(executeAction1.request, .scheduleRequestTimeout(for: request1, on: mockRequest1.eventLoop))
706+
707+
let mockRequest2 = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
708+
let request2 = HTTPConnectionPool.Request(mockRequest2)
709+
710+
let executeAction2 = state.executeRequest(request2)
711+
guard case .createConnection(let connectionID2, on: let connEL2) = executeAction2.connection else {
712+
return XCTFail("Expected to create a connection")
713+
}
714+
XCTAssert(mockRequest2.eventLoop === connEL2)
715+
716+
XCTAssertEqual(executeAction2.request, .scheduleRequestTimeout(for: request2, on: connEL1))
717+
718+
let failAction = state.failedToCreateNewConnection(HTTPClientError.httpProxyHandshakeTimeout, connectionID: connectionID1)
719+
guard case .scheduleBackoffTimer(connectionID1, backoff: _, on: let timerEL) = failAction.connection else {
720+
return XCTFail("Expected to create a backoff timer")
721+
}
722+
XCTAssert(timerEL === connEL2)
723+
XCTAssertEqual(failAction.request, .none)
724+
725+
let conn2 = HTTPConnectionPool.Connection.__testOnly_connection(id: connectionID2, eventLoop: connEL2)
726+
let createdAction = state.newHTTP1ConnectionCreated(conn2)
727+
728+
XCTAssertEqual(createdAction.request, .executeRequest(request1, conn2, cancelTimeout: true))
729+
XCTAssertEqual(createdAction.connection, .none)
730+
731+
let timeoutAction = state.timeoutRequest(request2.id)
732+
XCTAssertEqual(timeoutAction.request, .failRequest(request2, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false))
733+
XCTAssertEqual(timeoutAction.connection, .none)
734+
}
629735
}

Diff for: Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ class HTTPConnectionPoolTests: XCTestCase {
229229

230230
pool.executeRequest(requestBag)
231231
XCTAssertThrowsError(try requestBag.task.futureResult.wait()) {
232-
XCTAssertEqual($0 as? HTTPClientError, .getConnectionFromPoolTimeout)
232+
XCTAssertEqual($0 as? HTTPClientError, .proxyAuthenticationRequired)
233233
}
234234
XCTAssertGreaterThanOrEqual(httpBin.createdConnections, 8)
235235
XCTAssertEqual(httpBin.activeConnections, 0)

0 commit comments

Comments
 (0)