Skip to content

Commit db395e4

Browse files
committed
Preserve connection errors for user
1 parent 7bb58e5 commit db395e4

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

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

+6-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,9 @@ 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+
let error = self.lastConnectFailure ?? HTTPClientError.getConnectionFromPoolTimeout
226231
return .init(
227-
request: .failRequest(request, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false),
232+
request: .failRequest(request, error, cancelTimeout: false),
228233
connection: .none
229234
)
230235
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ extension HTTPConnectionPool_HTTP1StateMachineTests {
3838
("testConnectionPoolFullOfParkedConnectionsIsShutdownImmediately", testConnectionPoolFullOfParkedConnectionsIsShutdownImmediately),
3939
("testParkedConnectionTimesOutButIsAlsoClosedByRemote", testParkedConnectionTimesOutButIsAlsoClosedByRemote),
4040
("testConnectionBackoffVsShutdownRace", testConnectionBackoffVsShutdownRace),
41+
("testRequestThatTimesOutIsFailedWithLastConnectionCreationError", testRequestThatTimesOutIsFailedWithLastConnectionCreationError),
42+
("testRequestThatTimesOutAfterAConnectionWasEstablishedSuccessfullyTimesOutWithGenericError", testRequestThatTimesOutAfterAConnectionWasEstablishedSuccessfullyTimesOutWithGenericError),
4143
]
4244
}
4345
}

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

+83-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,86 @@ 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 testRequestThatTimesOutAfterAConnectionWasEstablishedSuccessfullyTimesOutWithGenericError() {
663+
let elg = EmbeddedEventLoopGroup(loops: 1)
664+
defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) }
665+
666+
var state = HTTPConnectionPool.StateMachine(
667+
eventLoopGroup: elg,
668+
idGenerator: .init(),
669+
maximumConcurrentHTTP1Connections: 6
670+
)
671+
672+
let mockRequest1 = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
673+
let request1 = HTTPConnectionPool.Request(mockRequest1)
674+
675+
let executeAction1 = state.executeRequest(request1)
676+
guard case .createConnection(let connectionID1, on: let connEL1) = executeAction1.connection else {
677+
return XCTFail("Expected to create a connection")
678+
}
679+
XCTAssert(mockRequest1.eventLoop === connEL1)
680+
681+
XCTAssertEqual(executeAction1.request, .scheduleRequestTimeout(for: request1, on: mockRequest1.eventLoop))
682+
683+
let mockRequest2 = MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel: false)
684+
let request2 = HTTPConnectionPool.Request(mockRequest2)
685+
686+
let executeAction2 = state.executeRequest(request2)
687+
guard case .createConnection(let connectionID2, on: let connEL2) = executeAction2.connection else {
688+
return XCTFail("Expected to create a connection")
689+
}
690+
XCTAssert(mockRequest2.eventLoop === connEL2)
691+
692+
XCTAssertEqual(executeAction2.request, .scheduleRequestTimeout(for: request2, on: connEL1))
693+
694+
let failAction = state.failedToCreateNewConnection(HTTPClientError.httpProxyHandshakeTimeout, connectionID: connectionID1)
695+
guard case .scheduleBackoffTimer(connectionID1, backoff: _, on: let timerEL) = failAction.connection else {
696+
return XCTFail("Expected to create a backoff timer")
697+
}
698+
XCTAssert(timerEL === connEL2)
699+
XCTAssertEqual(failAction.request, .none)
700+
701+
let conn2 = HTTPConnectionPool.Connection.__testOnly_connection(id: connectionID2, eventLoop: connEL2)
702+
let createdAction = state.newHTTP1ConnectionCreated(conn2)
703+
704+
XCTAssertEqual(createdAction.request, .executeRequest(request1, conn2, cancelTimeout: true))
705+
XCTAssertEqual(createdAction.connection, .none)
706+
707+
let timeoutAction = state.timeoutRequest(request2.id)
708+
XCTAssertEqual(timeoutAction.request, .failRequest(request2, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false))
709+
XCTAssertEqual(timeoutAction.connection, .none)
710+
}
629711
}

0 commit comments

Comments
 (0)