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..e9f3d5f70 100644 --- a/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift @@ -622,4 +622,43 @@ 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 + ) + + channel.write(request, promise: nil) + XCTAssertEqual(try channel.readOutbound(as: HTTPClientRequestPart.self), .head(request.requestHead)) + + channel.read() + try channel.writeInbound( + HTTPClientResponsePart.head( + .init(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "0")])) + ) + ) + channel.read() + try channel.writeInbound(HTTPClientResponsePart.end(nil)) + XCTAssertEqual(request.events.last?.kind, .receiveResponseEnd) + + channel.pipeline.fireErrorCaught(TestError()) + + XCTAssertFalse(channel.isActive) + XCTAssertEqual( + request.events.map(\.kind), + [ + .willExecuteRequest, .requestHeadSent, .resumeRequestBodyStream, .receiveResponseHead, + .receiveResponseEnd, + ] + ) + } + }