diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift index 2022fec4e..96fa711ec 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift @@ -244,9 +244,9 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler { self.request!.receiveResponseBodyParts(parts) case .failRequest(let error, let finalAction): - // We can force unwrap the request here, as we have just validated in the state machine, - // that the request object is still present. - self.request!.fail(error) + // The request may already have completed after receiving a response end while its body + // was still streaming. A subsequent stream error must not fail that request twice. + self.request?.fail(error) self.request = nil self.runTimeoutAction(.clearIdleReadTimeoutTimer, context: context) self.runTimeoutAction(.clearIdleWriteTimeoutTimer, context: context) diff --git a/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift b/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift index 07750eafd..a50a34e3a 100644 --- a/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift @@ -622,4 +622,37 @@ class HTTP2ClientRequestHandlerTests: XCTestCase { ) } + func testErrorAfterResponseEndWhileRequestBodyIsStreamingDoesNotCrash() throws { + struct TestError: Error {} + + let eventLoop = EmbeddedEventLoop() + let handler = HTTP2ClientRequestHandler(eventLoop: eventLoop) + let channel = EmbeddedChannel(handlers: [handler], loop: eventLoop) + try channel.connect(to: .init(ipAddress: "127.0.0.1", port: 80)).wait() + + let request = MockHTTPExecutableRequest( + head: .init(version: .http1_1, method: .POST, uri: "http://localhost/"), + framingMetadata: .init(connectionClose: false, body: .stream), + raiseErrorIfUnimplementedMethodIsCalled: false + ) + + try channel.writeOutbound(request) + XCTAssertEqual(try channel.readOutbound(as: HTTPClientRequestPart.self), .head(request.requestHead)) + + try channel.writeInbound(HTTPClientResponsePart.head(.init(version: .http1_1, status: .ok))) + try channel.writeInbound(HTTPClientResponsePart.end(nil)) + + channel.pipeline.fireErrorCaught(TestError()) + eventLoop.run() + + XCTAssertFalse(channel.isActive) + XCTAssertEqual( + request.events.map(\.kind), + [ + .willExecuteRequest, .requestHeadSent, .resumeRequestBodyStream, .receiveResponseHead, + .receiveResponseEnd, + ] + ) + } + }