Skip to content

Commit 062989e

Browse files
authored
Correctly reset our state after .sendEnd (#597)
Motivation In some cases, the last thing that happens in a request-response pair is that we send our HTTP/1.1 .end. This can happen when the peer has sent an early response, before we have finished uploading our body. When it does, we need to be diligent about cleaning up our connection state. Unfortunately, there was an edge in the HTTP1ConnectionStateMachine that processed .succeedRequest but that did not transition the state into either .idle or .closing. That was an error, and needed to be fixed. Modifications Transition to .idle when we're returning .succeedRequest(.sendRequestEnd). Result Fewer crashes
1 parent 0f21b44 commit 062989e

File tree

4 files changed

+61
-1
lines changed

4 files changed

+61
-1
lines changed

Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1ConnectionStateMachine.swift

+1
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ extension HTTP1ConnectionStateMachine.State {
412412
self = .closing
413413
newFinalAction = .close
414414
case .sendRequestEnd(let writePromise):
415+
self = .idle
415416
newFinalAction = .sendRequestEnd(writePromise)
416417
case .none:
417418
self = .idle

Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,11 @@ final class HTTP200DelayedHandler: ChannelInboundHandler {
12671267
let request = self.unwrapInboundIn(data)
12681268
switch request {
12691269
case .head:
1270-
break
1270+
// Once we have received one response, all further requests are responded to immediately.
1271+
if self.pendingBodyParts == nil {
1272+
context.writeAndFlush(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok))), promise: nil)
1273+
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
1274+
}
12711275
case .body:
12721276
if let pendingBodyParts = self.pendingBodyParts {
12731277
if pendingBodyParts > 0 {

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ extension HTTPClientTests {
130130
("testWeCloseConnectionsWhenConnectionCloseSetByServer", testWeCloseConnectionsWhenConnectionCloseSetByServer),
131131
("testBiDirectionalStreaming", testBiDirectionalStreaming),
132132
("testBiDirectionalStreamingEarly200", testBiDirectionalStreamingEarly200),
133+
("testBiDirectionalStreamingEarly200DoesntPreventUsFromSendingMoreRequests", testBiDirectionalStreamingEarly200DoesntPreventUsFromSendingMoreRequests),
133134
("testSynchronousHandshakeErrorReporting", testSynchronousHandshakeErrorReporting),
134135
("testFileDownloadChunked", testFileDownloadChunked),
135136
("testCloseWhileBackpressureIsExertedIsFine", testCloseWhileBackpressureIsExertedIsFine),

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+54
Original file line numberDiff line numberDiff line change
@@ -3021,6 +3021,60 @@ class HTTPClientTests: XCTestCase {
30213021
XCTAssertNil(try delegate.next().wait())
30223022
}
30233023

3024+
// This test is identical to the one above, except that we send another request immediately after. This is a regression
3025+
// test for https://github.com/swift-server/async-http-client/issues/595.
3026+
func testBiDirectionalStreamingEarly200DoesntPreventUsFromSendingMoreRequests() {
3027+
let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in HTTP200DelayedHandler(bodyPartsBeforeResponse: 1) }
3028+
defer { XCTAssertNoThrow(try httpBin.shutdown()) }
3029+
3030+
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)
3031+
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
3032+
let writeEL = eventLoopGroup.next()
3033+
3034+
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup))
3035+
defer { XCTAssertNoThrow(try httpClient.syncShutdown()) }
3036+
3037+
let body: HTTPClient.Body = .stream { writer in
3038+
let finalPromise = writeEL.makePromise(of: Void.self)
3039+
3040+
func writeLoop(_ writer: HTTPClient.Body.StreamWriter, index: Int) {
3041+
// always invoke from the wrong el to test thread safety
3042+
writeEL.preconditionInEventLoop()
3043+
3044+
if index >= 30 {
3045+
return finalPromise.succeed(())
3046+
}
3047+
3048+
let sent = ByteBuffer(integer: index)
3049+
writer.write(.byteBuffer(sent)).whenComplete { result in
3050+
switch result {
3051+
case .success:
3052+
writeEL.execute {
3053+
writeLoop(writer, index: index + 1)
3054+
}
3055+
3056+
case .failure(let error):
3057+
finalPromise.fail(error)
3058+
}
3059+
}
3060+
}
3061+
3062+
writeEL.execute {
3063+
writeLoop(writer, index: 0)
3064+
}
3065+
3066+
return finalPromise.futureResult
3067+
}
3068+
3069+
let request = try! HTTPClient.Request(url: "http://localhost:\(httpBin.port)", body: body)
3070+
let future = httpClient.execute(request: request)
3071+
XCTAssertNoThrow(try future.wait())
3072+
3073+
// Try another request
3074+
let future2 = httpClient.execute(request: request)
3075+
XCTAssertNoThrow(try future2.wait())
3076+
}
3077+
30243078
func testSynchronousHandshakeErrorReporting() throws {
30253079
// This only affects cases where we use NIOSSL.
30263080
guard !isTestingNIOTS() else { return }

0 commit comments

Comments
 (0)