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

‎Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1Connections.swift‎

Lines changed: 5 additions & 0 deletions
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+
varhasActiveConnections:Bool{
262+
self.connections.contains(where:{ $0.isIdle || $0.isLeased })
263+
}
264+
260265
func startingEventLoopConnections(on eventLoop:EventLoop)->Int{
261266
returnself.connections[self.overflowIndex..<self.connections.endIndex].reduce(into:0){ count, connection in
262267
guard connection.eventLoop === eventLoop else{return}

‎Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ extension HTTPConnectionPool{
2626

2727
privatevarconnections:HTTP1Connections
2828
privatevarfailedConsecutiveConnectionAttempts:Int=0
29+
/// the error from the last connection creation
30+
privatevarlastConnectFailure:Error?
2931

3032
privatevarrequests:RequestQueue
3133
privatevarstate:State=.running
@@ -136,12 +138,14 @@ extension HTTPConnectionPool{
136138

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

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

146150
switchself.state {
147151
case.running:
@@ -223,8 +227,14 @@ extension HTTPConnectionPool{
223227
mutatingfunc timeoutRequest(_ requestID:Request.ID)->Action{
224228
// 1. check requests in queue
225229
iflet request =self.requests.remove(requestID){
230+
varerror:Error=HTTPClientError.getConnectionFromPoolTimeout
231+
iflet lastError =self.lastConnectFailure {
232+
error = lastError
233+
}elseif !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
}

‎Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP1StateTests+XCTest.swift‎

Lines changed: 3 additions & 0 deletions
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
}

‎Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP1StateTests.swift‎

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase{
153153
returnXCTFail("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+
letelg=EmbeddedEventLoopGroup(loops:1)
632+
defer{XCTAssertNoThrow(try elg.syncShutdownGracefully())}
633+
634+
varstate=HTTPConnectionPool.StateMachine(
635+
eventLoopGroup: elg,
636+
idGenerator:.init(),
637+
maximumConcurrentHTTP1Connections:6
638+
)
639+
640+
letmockRequest=MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel:false)
641+
letrequest=HTTPConnectionPool.Request(mockRequest)
642+
643+
letexecuteAction= state.executeRequest(request)
644+
guard case .createConnection(let connectionID, on:let connEL)= executeAction.connection else{
645+
returnXCTFail("Expected to create a connection")
646+
}
647+
648+
XCTAssertEqual(executeAction.request,.scheduleRequestTimeout(for: request, on: mockRequest.eventLoop))
649+
650+
letfailAction= state.failedToCreateNewConnection(HTTPClientError.httpProxyHandshakeTimeout, connectionID: connectionID)
651+
guard case .scheduleBackoffTimer(connectionID, backoff: _, on:let timerEL)= failAction.connection else{
652+
returnXCTFail("Expected to create a backoff timer")
653+
}
654+
XCTAssert(timerEL === connEL)
655+
XCTAssertEqual(failAction.request,.none)
656+
657+
lettimeoutAction= 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+
leteventLoop=EmbeddedEventLoop()
664+
defer{XCTAssertNoThrow(try eventLoop.syncShutdownGracefully())}
665+
666+
varstate=HTTPConnectionPool.StateMachine(
667+
eventLoopGroup: eventLoop,
668+
idGenerator:.init(),
669+
maximumConcurrentHTTP1Connections:6
670+
)
671+
672+
letmockRequest=MockHTTPRequest(eventLoop: eventLoop.next(), requiresEventLoopForChannel:false)
673+
letrequest=HTTPConnectionPool.Request(mockRequest)
674+
675+
letexecuteAction= state.executeRequest(request)
676+
guard case .createConnection(_, on: _)= executeAction.connection else{
677+
returnXCTFail("Expected to create a connection")
678+
}
679+
XCTAssertEqual(executeAction.request,.scheduleRequestTimeout(for: request, on: mockRequest.eventLoop))
680+
681+
lettimeoutAction= 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+
letelg=EmbeddedEventLoopGroup(loops:1)
688+
defer{XCTAssertNoThrow(try elg.syncShutdownGracefully())}
689+
690+
varstate=HTTPConnectionPool.StateMachine(
691+
eventLoopGroup: elg,
692+
idGenerator:.init(),
693+
maximumConcurrentHTTP1Connections:6
694+
)
695+
696+
letmockRequest1=MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel:false)
697+
letrequest1=HTTPConnectionPool.Request(mockRequest1)
698+
699+
letexecuteAction1= state.executeRequest(request1)
700+
guard case .createConnection(let connectionID1, on:let connEL1)= executeAction1.connection else{
701+
returnXCTFail("Expected to create a connection")
702+
}
703+
XCTAssert(mockRequest1.eventLoop === connEL1)
704+
705+
XCTAssertEqual(executeAction1.request,.scheduleRequestTimeout(for: request1, on: mockRequest1.eventLoop))
706+
707+
letmockRequest2=MockHTTPRequest(eventLoop: elg.next(), requiresEventLoopForChannel:false)
708+
letrequest2=HTTPConnectionPool.Request(mockRequest2)
709+
710+
letexecuteAction2= state.executeRequest(request2)
711+
guard case .createConnection(let connectionID2, on:let connEL2)= executeAction2.connection else{
712+
returnXCTFail("Expected to create a connection")
713+
}
714+
XCTAssert(mockRequest2.eventLoop === connEL2)
715+
716+
XCTAssertEqual(executeAction2.request,.scheduleRequestTimeout(for: request2, on: connEL1))
717+
718+
letfailAction= state.failedToCreateNewConnection(HTTPClientError.httpProxyHandshakeTimeout, connectionID: connectionID1)
719+
guard case .scheduleBackoffTimer(connectionID1, backoff: _, on:let timerEL)= failAction.connection else{
720+
returnXCTFail("Expected to create a backoff timer")
721+
}
722+
XCTAssert(timerEL === connEL2)
723+
XCTAssertEqual(failAction.request,.none)
724+
725+
letconn2=HTTPConnectionPool.Connection.__testOnly_connection(id: connectionID2, eventLoop: connEL2)
726+
letcreatedAction= state.newHTTP1ConnectionCreated(conn2)
727+
728+
XCTAssertEqual(createdAction.request,.executeRequest(request1, conn2, cancelTimeout:true))
729+
XCTAssertEqual(createdAction.connection,.none)
730+
731+
lettimeoutAction= state.timeoutRequest(request2.id)
732+
XCTAssertEqual(timeoutAction.request,.failRequest(request2,HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout:false))
733+
XCTAssertEqual(timeoutAction.connection,.none)
734+
}
629735
}

‎Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift‎

Lines changed: 1 addition & 1 deletion
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)