Skip to content

Commit cbf5330

Browse files
authored
Fix race between connection close and scheduling new request (#546) (#548)
1 parent eb7b1fb commit cbf5330

File tree

3 files changed

+41
-8
lines changed

3 files changed

+41
-8
lines changed

‎Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ConnectionStateMachine.swift‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,17 +158,35 @@ struct HTTP1ConnectionStateMachine{
158158
head:HTTPRequestHead,
159159
metadata:RequestFramingMetadata
160160
)->Action{
161-
guard case .idle =self.state else{
161+
switchself.state {
162+
case.initialized,.closing,.inRequest:
163+
// These states are unreachable as the connection pool state machine has put the
164+
// connection into these states. In other words the connection pool state machine must
165+
// be aware about these states before the connection itself. For this reason the
166+
// connection pool state machine must not send a new request to the connection, if the
167+
// connection is `.initialized`, `.closing` or `.inRequest`
162168
preconditionFailure("Invalid state: \(self.state)")
163-
}
164169

165-
varrequestStateMachine=HTTPRequestStateMachine(isChannelWritable:self.isChannelWritable)
166-
letaction= requestStateMachine.startRequest(head: head, metadata: metadata)
170+
case.closed:
171+
// The remote may have closed the connection and the connection pool state machine
172+
// was not updated yet because of a race condition. New request vs. marking connection
173+
// as closed.
174+
//
175+
// TODO: AHC should support a fast rescheduling mechanism here.
176+
return.failRequest(HTTPClientError.remoteConnectionClosed,.none)
177+
178+
case.idle:
179+
varrequestStateMachine=HTTPRequestStateMachine(isChannelWritable:self.isChannelWritable)
180+
letaction= requestStateMachine.startRequest(head: head, metadata: metadata)
167181

168-
// by default we assume a persistent connection. however in `requestVerified`, we read the
169-
// "connection" header.
170-
self.state =.inRequest(requestStateMachine, close: metadata.connectionClose)
171-
returnself.state.modify(with: action)
182+
// by default we assume a persistent connection. however in `requestVerified`, we read the
183+
// "connection" header.
184+
self.state =.inRequest(requestStateMachine, close: metadata.connectionClose)
185+
returnself.state.modify(with: action)
186+
187+
case.modifying:
188+
preconditionFailure("Invalid state: \(self.state)")
189+
}
172190
}
173191

174192
mutatingfunc requestStreamPartReceived(_ part:IOData)->Action{

‎Tests/AsyncHTTPClientTests/HTTP1ConnectionStateMachineTests+XCTest.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ extension HTTP1ConnectionStateMachineTests{
4242
("testConnectionIsClosedIfErrorHappensWhileInRequest", testConnectionIsClosedIfErrorHappensWhileInRequest),
4343
("testConnectionIsClosedAfterSwitchingProtocols", testConnectionIsClosedAfterSwitchingProtocols),
4444
("testWeDontCrashAfterEarlyHintsAndConnectionClose", testWeDontCrashAfterEarlyHintsAndConnectionClose),
45+
("testWeDontCrashInRaceBetweenSchedulingNewRequestAndConnectionClose", testWeDontCrashInRaceBetweenSchedulingNewRequestAndConnectionClose),
4546
]
4647
}
4748
}

‎Tests/AsyncHTTPClientTests/HTTP1ConnectionStateMachineTests.swift‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,20 @@ class HTTP1ConnectionStateMachineTests: XCTestCase{
267267
XCTAssertEqual(state.channelRead(.head(responseHead)),.wait)
268268
XCTAssertEqual(state.channelInactive(),.failRequest(HTTPClientError.remoteConnectionClosed,.none))
269269
}
270+
271+
func testWeDontCrashInRaceBetweenSchedulingNewRequestAndConnectionClose(){
272+
varstate=HTTP1ConnectionStateMachine()
273+
XCTAssertEqual(state.channelActive(isWritable:true),.fireChannelActive)
274+
XCTAssertEqual(state.channelInactive(),.fireChannelInactive)
275+
276+
letrequestHead=HTTPRequestHead(version:.http1_1, method:.GET, uri:"/")
277+
letmetadata=RequestFramingMetadata(connectionClose:false, body:.fixedSize(0))
278+
letnewRequestAction= state.runNewRequest(head: requestHead, metadata: metadata)
279+
guard case .failRequest(let error,.none)= newRequestAction else{
280+
returnXCTFail("Unexpected test case")
281+
}
282+
XCTAssertEqual(error as?HTTPClientError,.remoteConnectionClosed)
283+
}
270284
}
271285

272286
extensionHTTP1ConnectionStateMachine.Action:Equatable{

0 commit comments

Comments
(0)