From e097940a498f08db75f5c6c52f2127b407bdb3f0 Mon Sep 17 00:00:00 2001 From: David Nadoba <d_nadoba@apple.com> Date: Wed, 24 Nov 2021 07:07:27 +0100 Subject: [PATCH 1/8] fix bodyLengthMissmatch error handling --- .../HTTPRequestStateMachine.swift | 6 ++--- .../HTTPRequestStateMachineTests+XCTest.swift | 2 ++ .../HTTPRequestStateMachineTests.swift | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift index 2d6ad5dd8..a3a9c38e0 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift @@ -198,14 +198,14 @@ struct HTTPRequestStateMachine { self.state = .failed(error) return .failRequest(error, .none) - case .running(.streaming, .waitingForHead), + case .running(.streaming, .waitingForHead) where error as? NIOSSLError == .uncleanShutdown, .running(.endSent, .waitingForHead) where error as? NIOSSLError == .uncleanShutdown: // if we received a NIOSSL.uncleanShutdown before we got an answer we should handle // this like a normal connection close. We will receive a call to channelInactive after // this error. return .wait - case .running(.streaming, .receivingBody(let responseHead, _)), + case .running(.streaming, .receivingBody(let responseHead, _)) where error as? NIOSSLError == .uncleanShutdown, .running(.endSent, .receivingBody(let responseHead, _)) where error as? NIOSSLError == .uncleanShutdown: // This code is only reachable for request and responses, which we expect to have a body. // We depend on logic from the HTTPResponseDecoder here. The decoder will emit an @@ -270,7 +270,7 @@ struct HTTPRequestStateMachine { if let expected = expectedBodyLength, sentBodyBytes + part.readableBytes > expected { let error = HTTPClientError.bodyLengthMismatch - + self.state = .failed(error) return .failRequest(error, .close) } diff --git a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests+XCTest.swift index 3dd6c7b30..b54865fd8 100644 --- a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests+XCTest.swift @@ -54,6 +54,8 @@ extension HTTPRequestStateMachineTests { ("testCanReadHTTP1_0ResponseWithBody", testCanReadHTTP1_0ResponseWithBody), ("testFailHTTP1_0RequestThatIsStillUploading", testFailHTTP1_0RequestThatIsStillUploading), ("testFailHTTP1RequestWithoutContentLengthWithNIOSSLErrorUncleanShutdown", testFailHTTP1RequestWithoutContentLengthWithNIOSSLErrorUncleanShutdown), + ("testNIOSSLErrorUncleanShutdownShouldBeTreatedAsRemoteConnectionCloseWhileInWaitingForHeadState", testNIOSSLErrorUncleanShutdownShouldBeTreatedAsRemoteConnectionCloseWhileInWaitingForHeadState), + ("testArbitraryErrorShouldBeTreatedAsARequestFailureWhileInWaitingForHeadState", testArbitraryErrorShouldBeTreatedAsARequestFailureWhileInWaitingForHeadState), ("testFailHTTP1RequestWithContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt", testFailHTTP1RequestWithContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt), ("testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForDemand", testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForDemand), ("testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForRead", testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForRead), diff --git a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift index 3e92dc87f..edbd3a002 100644 --- a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift @@ -82,6 +82,9 @@ class HTTPRequestStateMachineTests: XCTestCase { } XCTAssertEqual(error as? HTTPClientError, .bodyLengthMismatch) + + // if another error happens the new one is ignored + XCTAssertEqual(state.errorHappened(HTTPClientError.remoteConnectionClosed), .wait) } func testPOSTContentLengthIsTooShort() { @@ -522,6 +525,27 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelInactive(), .wait) } + func testNIOSSLErrorUncleanShutdownShouldBeTreatedAsRemoteConnectionCloseWhileInWaitingForHeadState() { + var state = HTTPRequestStateMachine(isChannelWritable: true) + let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") + let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) + XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) + + XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait) + XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + } + + func testArbitraryErrorShouldBeTreatedAsARequestFailureWhileInWaitingForHeadState() { + struct ArbitraryError: Error {} + var state = HTTPRequestStateMachine(isChannelWritable: true) + let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") + let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) + XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) + + XCTAssertEqual(state.errorHappened(ArbitraryError()), .failRequest(ArbitraryError(), .close)) + XCTAssertEqual(state.channelInactive(), .wait) + } + func testFailHTTP1RequestWithContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt() { var state = HTTPRequestStateMachine(isChannelWritable: true) let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") From 3a6a256f6ae105ba7b0f4fb716f559380197bc88 Mon Sep 17 00:00:00 2001 From: David Nadoba <d_nadoba@apple.com> Date: Wed, 24 Nov 2021 10:21:19 +0100 Subject: [PATCH 2/8] check error through pattern matching because `HTTPRequestStateMachine.Action` Equatable conformance ignores the error completly --- .../HTTPRequestStateMachineTests.swift | 87 +++++++++++++++---- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift index edbd3a002..5df644cfa 100644 --- a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift @@ -209,7 +209,10 @@ class HTTPRequestStateMachineTests: XCTestCase { let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7)) XCTAssertEqual(state.requestStreamPartReceived(part1), .sendBodyPart(part1)) - XCTAssertEqual(state.requestStreamFinished(), .failRequest(HTTPClientError.bodyLengthMismatch, .close)) + guard case .failRequest(let error, .close) = state.requestStreamFinished() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .bodyLengthMismatch) XCTAssertEqual(state.channelInactive(), .wait) } @@ -227,7 +230,10 @@ class HTTPRequestStateMachineTests: XCTestCase { let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7)) XCTAssertEqual(state.requestStreamPartReceived(part1), .sendBodyPart(part1)) - XCTAssertEqual(state.requestStreamFinished(), .failRequest(HTTPClientError.bodyLengthMismatch, .close)) + guard case .failRequest(let error, .close) = state.requestStreamFinished() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .bodyLengthMismatch) XCTAssertEqual(state.channelRead(.end(nil)), .wait) } @@ -252,7 +258,10 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + guard case .failRequest(let error, .none) = state.channelInactive() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) } func testResponseReadingWithBackpressure() { @@ -342,7 +351,10 @@ class HTTPRequestStateMachineTests: XCTestCase { func testCancellingARequestInStateInitializedKeepsTheConnectionAlive() { var state = HTTPRequestStateMachine(isChannelWritable: false) - XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none)) + guard case .failRequest(let error, .none) = state.requestCancelled() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .cancelled) } func testCancellingARequestBeforeBeingSendKeepsTheConnectionAlive() { @@ -350,7 +362,10 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait) - XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none)) + guard case .failRequest(let error, .none) = state.requestCancelled() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .cancelled) } func testConnectionBecomesWritableBeforeFirstRequest() { @@ -376,7 +391,10 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .close)) + guard case .failRequest(let error, .close) = state.requestCancelled() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .cancelled) } func testRemoteSuddenlyClosesTheConnection() { @@ -384,7 +402,10 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: .init([("content-length", "4")])) let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true)) - XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.remoteConnectionClosed, .close)) + guard case .failRequest(let error, .close) = state.requestCancelled() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .cancelled) XCTAssertEqual(state.requestStreamPartReceived(.byteBuffer(.init(bytes: 1...3))), .wait) } @@ -398,7 +419,10 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false)) let part0 = ByteBuffer(bytes: 0...3) XCTAssertEqual(state.channelRead(.body(part0)), .wait) - XCTAssertEqual(state.idleReadTimeoutTriggered(), .failRequest(HTTPClientError.readTimeout, .close)) + guard case .failRequest(let error, .close) = state.idleReadTimeoutTriggered() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .readTimeout) XCTAssertEqual(state.channelRead(.body(ByteBuffer(bytes: 4...7))), .wait) XCTAssertEqual(state.channelRead(.body(ByteBuffer(bytes: 8...11))), .wait) XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait) @@ -451,7 +475,10 @@ class HTTPRequestStateMachineTests: XCTestCase { let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - XCTAssertEqual(state.errorHappened(HTTPParserError.invalidChunkSize), .failRequest(HTTPParserError.invalidChunkSize, .close)) + guard case .failRequest(let error, .close) = state.errorHappened(HTTPParserError.invalidChunkSize) else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPParserError, .invalidChunkSize) XCTAssertEqual(state.requestCancelled(), .wait, "A cancellation that happens to late is ignored") } @@ -505,7 +532,10 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.read(), .read) XCTAssertEqual(state.channelReadComplete(), .wait) XCTAssertEqual(state.channelRead(.body(body)), .wait) - XCTAssertEqual(state.channelRead(.end(nil)), .failRequest(HTTPClientError.remoteConnectionClosed, .close)) + guard case .failRequest(let error, .close) = state.channelRead(.end(nil)) else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) XCTAssertEqual(state.channelInactive(), .wait) } @@ -520,7 +550,10 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false)) XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait) XCTAssertEqual(state.channelRead(.body(body)), .wait) - XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .failRequest(NIOSSLError.uncleanShutdown, .close)) + guard case .failRequest(let error, .close) = state.errorHappened(NIOSSLError.uncleanShutdown) else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? NIOSSLError, .uncleanShutdown) XCTAssertEqual(state.channelRead(.end(nil)), .wait) XCTAssertEqual(state.channelInactive(), .wait) } @@ -532,7 +565,11 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + + guard case .failRequest(let error, .none) = state.channelInactive() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) } func testArbitraryErrorShouldBeTreatedAsARequestFailureWhileInWaitingForHeadState() { @@ -541,8 +578,10 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - - XCTAssertEqual(state.errorHappened(ArbitraryError()), .failRequest(ArbitraryError(), .close)) + guard case .failRequest(let error, .close) = state.errorHappened(ArbitraryError()) else { + return XCTFail("unexpected action") + } + XCTAssertTrue(error is ArbitraryError) XCTAssertEqual(state.channelInactive(), .wait) } @@ -560,7 +599,10 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(body)), .wait) XCTAssertEqual(state.channelReadComplete(), .forwardResponseBodyParts([body])) XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait) - XCTAssertEqual(state.errorHappened(HTTPParserError.invalidEOFState), .failRequest(HTTPParserError.invalidEOFState, .close)) + guard case .failRequest(let error, .close) = state.errorHappened(HTTPParserError.invalidEOFState) else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPParserError, .invalidEOFState) XCTAssertEqual(state.channelInactive(), .wait) } @@ -582,7 +624,10 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + guard case .failRequest(let error, .none) = state.channelInactive() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForRead() { @@ -603,7 +648,10 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + guard case .failRequest(let error, .none) = state.channelInactive() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForReadAndDemand() { @@ -623,7 +671,10 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + guard case .failRequest(let error, .none) = state.channelInactive() else { + return XCTFail("unexpected action") + } + XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForReadAndDemandMultipleTimes() { From 0d8930ccf7e830254478e51a8365636033919e9e Mon Sep 17 00:00:00 2001 From: David Nadoba <d_nadoba@apple.com> Date: Wed, 24 Nov 2021 11:02:34 +0100 Subject: [PATCH 3/8] Revert "check error through pattern matching" This reverts commit 3a6a256f6ae105ba7b0f4fb716f559380197bc88. --- .../HTTPRequestStateMachineTests.swift | 87 ++++--------------- 1 file changed, 18 insertions(+), 69 deletions(-) diff --git a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift index 5df644cfa..edbd3a002 100644 --- a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift @@ -209,10 +209,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7)) XCTAssertEqual(state.requestStreamPartReceived(part1), .sendBodyPart(part1)) - guard case .failRequest(let error, .close) = state.requestStreamFinished() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .bodyLengthMismatch) + XCTAssertEqual(state.requestStreamFinished(), .failRequest(HTTPClientError.bodyLengthMismatch, .close)) XCTAssertEqual(state.channelInactive(), .wait) } @@ -230,10 +227,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7)) XCTAssertEqual(state.requestStreamPartReceived(part1), .sendBodyPart(part1)) - guard case .failRequest(let error, .close) = state.requestStreamFinished() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .bodyLengthMismatch) + XCTAssertEqual(state.requestStreamFinished(), .failRequest(HTTPClientError.bodyLengthMismatch, .close)) XCTAssertEqual(state.channelRead(.end(nil)), .wait) } @@ -258,10 +252,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait) - guard case .failRequest(let error, .none) = state.channelInactive() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) + XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) } func testResponseReadingWithBackpressure() { @@ -351,10 +342,7 @@ class HTTPRequestStateMachineTests: XCTestCase { func testCancellingARequestInStateInitializedKeepsTheConnectionAlive() { var state = HTTPRequestStateMachine(isChannelWritable: false) - guard case .failRequest(let error, .none) = state.requestCancelled() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .cancelled) + XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none)) } func testCancellingARequestBeforeBeingSendKeepsTheConnectionAlive() { @@ -362,10 +350,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait) - guard case .failRequest(let error, .none) = state.requestCancelled() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .cancelled) + XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none)) } func testConnectionBecomesWritableBeforeFirstRequest() { @@ -391,10 +376,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - guard case .failRequest(let error, .close) = state.requestCancelled() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .cancelled) + XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .close)) } func testRemoteSuddenlyClosesTheConnection() { @@ -402,10 +384,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: .init([("content-length", "4")])) let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true)) - guard case .failRequest(let error, .close) = state.requestCancelled() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .cancelled) + XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.remoteConnectionClosed, .close)) XCTAssertEqual(state.requestStreamPartReceived(.byteBuffer(.init(bytes: 1...3))), .wait) } @@ -419,10 +398,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false)) let part0 = ByteBuffer(bytes: 0...3) XCTAssertEqual(state.channelRead(.body(part0)), .wait) - guard case .failRequest(let error, .close) = state.idleReadTimeoutTriggered() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .readTimeout) + XCTAssertEqual(state.idleReadTimeoutTriggered(), .failRequest(HTTPClientError.readTimeout, .close)) XCTAssertEqual(state.channelRead(.body(ByteBuffer(bytes: 4...7))), .wait) XCTAssertEqual(state.channelRead(.body(ByteBuffer(bytes: 8...11))), .wait) XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait) @@ -475,10 +451,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - guard case .failRequest(let error, .close) = state.errorHappened(HTTPParserError.invalidChunkSize) else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPParserError, .invalidChunkSize) + XCTAssertEqual(state.errorHappened(HTTPParserError.invalidChunkSize), .failRequest(HTTPParserError.invalidChunkSize, .close)) XCTAssertEqual(state.requestCancelled(), .wait, "A cancellation that happens to late is ignored") } @@ -532,10 +505,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.read(), .read) XCTAssertEqual(state.channelReadComplete(), .wait) XCTAssertEqual(state.channelRead(.body(body)), .wait) - guard case .failRequest(let error, .close) = state.channelRead(.end(nil)) else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) + XCTAssertEqual(state.channelRead(.end(nil)), .failRequest(HTTPClientError.remoteConnectionClosed, .close)) XCTAssertEqual(state.channelInactive(), .wait) } @@ -550,10 +520,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false)) XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait) XCTAssertEqual(state.channelRead(.body(body)), .wait) - guard case .failRequest(let error, .close) = state.errorHappened(NIOSSLError.uncleanShutdown) else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? NIOSSLError, .uncleanShutdown) + XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .failRequest(NIOSSLError.uncleanShutdown, .close)) XCTAssertEqual(state.channelRead(.end(nil)), .wait) XCTAssertEqual(state.channelInactive(), .wait) } @@ -565,11 +532,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait) - - guard case .failRequest(let error, .none) = state.channelInactive() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) + XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) } func testArbitraryErrorShouldBeTreatedAsARequestFailureWhileInWaitingForHeadState() { @@ -578,10 +541,8 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - guard case .failRequest(let error, .close) = state.errorHappened(ArbitraryError()) else { - return XCTFail("unexpected action") - } - XCTAssertTrue(error is ArbitraryError) + + XCTAssertEqual(state.errorHappened(ArbitraryError()), .failRequest(ArbitraryError(), .close)) XCTAssertEqual(state.channelInactive(), .wait) } @@ -599,10 +560,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(body)), .wait) XCTAssertEqual(state.channelReadComplete(), .forwardResponseBodyParts([body])) XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait) - guard case .failRequest(let error, .close) = state.errorHappened(HTTPParserError.invalidEOFState) else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPParserError, .invalidEOFState) + XCTAssertEqual(state.errorHappened(HTTPParserError.invalidEOFState), .failRequest(HTTPParserError.invalidEOFState, .close)) XCTAssertEqual(state.channelInactive(), .wait) } @@ -624,10 +582,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - guard case .failRequest(let error, .none) = state.channelInactive() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) + XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForRead() { @@ -648,10 +603,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - guard case .failRequest(let error, .none) = state.channelInactive() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) + XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForReadAndDemand() { @@ -671,10 +623,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - guard case .failRequest(let error, .none) = state.channelInactive() else { - return XCTFail("unexpected action") - } - XCTAssertEqual(error as? HTTPClientError, .remoteConnectionClosed) + XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForReadAndDemandMultipleTimes() { From 0bf82398ad776c040de2c62d89f348787a3068ac Mon Sep 17 00:00:00 2001 From: David Nadoba <d_nadoba@apple.com> Date: Wed, 24 Nov 2021 11:11:25 +0100 Subject: [PATCH 4/8] add and use new assertFailRequest method --- .../HTTPRequestStateMachineTests.swift | 69 +++++++++++-------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift index edbd3a002..ab55345c9 100644 --- a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift @@ -76,12 +76,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let part1 = IOData.byteBuffer(ByteBuffer(bytes: [0, 1, 2, 3])) XCTAssertEqual(state.requestStreamPartReceived(part0), .sendBodyPart(part0)) - let failAction = state.requestStreamPartReceived(part1) - guard case .failRequest(let error, .close) = failAction else { - return XCTFail("Unexpected action: \(failAction)") - } - - XCTAssertEqual(error as? HTTPClientError, .bodyLengthMismatch) + state.requestStreamPartReceived(part1).assertFailRequest(HTTPClientError.bodyLengthMismatch, .close) // if another error happens the new one is ignored XCTAssertEqual(state.errorHappened(HTTPClientError.remoteConnectionClosed), .wait) @@ -95,12 +90,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0, 1, 2, 3])) XCTAssertEqual(state.requestStreamPartReceived(part0), .sendBodyPart(part0)) - let failAction = state.requestStreamFinished() - guard case .failRequest(let error, .close) = failAction else { - return XCTFail("Unexpected action: \(failAction)") - } - - XCTAssertEqual(error as? HTTPClientError, .bodyLengthMismatch) + state.requestStreamFinished().assertFailRequest(HTTPClientError.bodyLengthMismatch, .close) } func testRequestBodyStreamIsCancelledIfServerRespondsWith301() { @@ -209,7 +199,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7)) XCTAssertEqual(state.requestStreamPartReceived(part1), .sendBodyPart(part1)) - XCTAssertEqual(state.requestStreamFinished(), .failRequest(HTTPClientError.bodyLengthMismatch, .close)) + state.requestStreamFinished().assertFailRequest(HTTPClientError.bodyLengthMismatch, .close) XCTAssertEqual(state.channelInactive(), .wait) } @@ -227,7 +217,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7)) XCTAssertEqual(state.requestStreamPartReceived(part1), .sendBodyPart(part1)) - XCTAssertEqual(state.requestStreamFinished(), .failRequest(HTTPClientError.bodyLengthMismatch, .close)) + state.requestStreamFinished().assertFailRequest(HTTPClientError.bodyLengthMismatch, .close) XCTAssertEqual(state.channelRead(.end(nil)), .wait) } @@ -252,7 +242,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + state.channelInactive().assertFailRequest(HTTPClientError.remoteConnectionClosed, .none) } func testResponseReadingWithBackpressure() { @@ -342,7 +332,7 @@ class HTTPRequestStateMachineTests: XCTestCase { func testCancellingARequestInStateInitializedKeepsTheConnectionAlive() { var state = HTTPRequestStateMachine(isChannelWritable: false) - XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none)) + state.requestCancelled().assertFailRequest(HTTPClientError.cancelled, .none) } func testCancellingARequestBeforeBeingSendKeepsTheConnectionAlive() { @@ -350,7 +340,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait) - XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none)) + state.requestCancelled().assertFailRequest(HTTPClientError.cancelled, .none) } func testConnectionBecomesWritableBeforeFirstRequest() { @@ -376,7 +366,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .close)) + state.requestCancelled().assertFailRequest(HTTPClientError.cancelled, .close) } func testRemoteSuddenlyClosesTheConnection() { @@ -384,7 +374,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: .init([("content-length", "4")])) let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true)) - XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.remoteConnectionClosed, .close)) + state.requestCancelled().assertFailRequest(HTTPClientError.cancelled, .close) XCTAssertEqual(state.requestStreamPartReceived(.byteBuffer(.init(bytes: 1...3))), .wait) } @@ -398,7 +388,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false)) let part0 = ByteBuffer(bytes: 0...3) XCTAssertEqual(state.channelRead(.body(part0)), .wait) - XCTAssertEqual(state.idleReadTimeoutTriggered(), .failRequest(HTTPClientError.readTimeout, .close)) + state.idleReadTimeoutTriggered().assertFailRequest(HTTPClientError.readTimeout, .close) XCTAssertEqual(state.channelRead(.body(ByteBuffer(bytes: 4...7))), .wait) XCTAssertEqual(state.channelRead(.body(ByteBuffer(bytes: 8...11))), .wait) XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait) @@ -451,7 +441,7 @@ class HTTPRequestStateMachineTests: XCTestCase { let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - XCTAssertEqual(state.errorHappened(HTTPParserError.invalidChunkSize), .failRequest(HTTPParserError.invalidChunkSize, .close)) + state.errorHappened(HTTPParserError.invalidChunkSize).assertFailRequest(HTTPParserError.invalidChunkSize, .close) XCTAssertEqual(state.requestCancelled(), .wait, "A cancellation that happens to late is ignored") } @@ -505,7 +495,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.read(), .read) XCTAssertEqual(state.channelReadComplete(), .wait) XCTAssertEqual(state.channelRead(.body(body)), .wait) - XCTAssertEqual(state.channelRead(.end(nil)), .failRequest(HTTPClientError.remoteConnectionClosed, .close)) + state.channelRead(.end(nil)).assertFailRequest(HTTPClientError.remoteConnectionClosed, .close) XCTAssertEqual(state.channelInactive(), .wait) } @@ -520,7 +510,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false)) XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait) XCTAssertEqual(state.channelRead(.body(body)), .wait) - XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .failRequest(NIOSSLError.uncleanShutdown, .close)) + state.errorHappened(NIOSSLError.uncleanShutdown).assertFailRequest(NIOSSLError.uncleanShutdown, .close) XCTAssertEqual(state.channelRead(.end(nil)), .wait) XCTAssertEqual(state.channelInactive(), .wait) } @@ -532,17 +522,17 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + state.channelInactive().assertFailRequest(HTTPClientError.remoteConnectionClosed, .none) } func testArbitraryErrorShouldBeTreatedAsARequestFailureWhileInWaitingForHeadState() { - struct ArbitraryError: Error {} + struct ArbitraryError: Error, Equatable {} var state = HTTPRequestStateMachine(isChannelWritable: true) let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/") let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0)) XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false)) - XCTAssertEqual(state.errorHappened(ArbitraryError()), .failRequest(ArbitraryError(), .close)) + state.errorHappened(ArbitraryError()).assertFailRequest(ArbitraryError(), .close) XCTAssertEqual(state.channelInactive(), .wait) } @@ -560,7 +550,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(body)), .wait) XCTAssertEqual(state.channelReadComplete(), .forwardResponseBodyParts([body])) XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait) - XCTAssertEqual(state.errorHappened(HTTPParserError.invalidEOFState), .failRequest(HTTPParserError.invalidEOFState, .close)) + state.errorHappened(HTTPParserError.invalidEOFState).assertFailRequest(HTTPParserError.invalidEOFState, .close) XCTAssertEqual(state.channelInactive(), .wait) } @@ -582,7 +572,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + state.channelInactive().assertFailRequest(HTTPClientError.remoteConnectionClosed, .none) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForRead() { @@ -603,7 +593,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + state.channelInactive().assertFailRequest(HTTPClientError.remoteConnectionClosed, .none) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForReadAndDemand() { @@ -623,7 +613,7 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: " baz lightyear"))), .wait) XCTAssertEqual(state.channelReadComplete(), .wait) - XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none)) + state.channelInactive().assertFailRequest(HTTPClientError.remoteConnectionClosed, .none) } func testFailHTTPRequestWithContentLengthBecauseOfChannelInactiveWaitingForReadAndDemandMultipleTimes() { @@ -700,3 +690,22 @@ extension HTTPRequestStateMachine.Action: Equatable { } } } + +extension HTTPRequestStateMachine.Action { + fileprivate func assertFailRequest<Error>( + _ expectedError: Error, + _ expectedFinalStreamAction: HTTPRequestStateMachine.Action.FinalStreamAction, + file: StaticString = #file, + line: UInt = #line + ) where Error: Swift.Error & Equatable { + guard case .failRequest(let actualError, let actualFinalStreamAction) = self else { + return XCTFail("expected .failRequest(\(expectedError), \(expectedFinalStreamAction)) but got \(self)", file: file, line: line) + } + if let actualError = actualError as? Error { + XCTAssertEqual(actualError, expectedError, file: file, line: line) + } else { + XCTFail("\(actualError) is not equal to \(expectedError)", file: file, line: line) + } + XCTAssertEqual(actualFinalStreamAction, expectedFinalStreamAction, file: file, line: line) + } +} From 99aaeecf771c3129959734904f483b4c11cc5be7 Mon Sep 17 00:00:00 2001 From: David Nadoba <d_nadoba@apple.com> Date: Wed, 24 Nov 2021 15:21:28 +0100 Subject: [PATCH 5/8] extract where clause into seperate if + switch --- .../HTTPRequestStateMachine.swift | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift index a3a9c38e0..7619b9214 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift @@ -190,6 +190,49 @@ struct HTTPRequestStateMachine { } mutating func errorHappened(_ error: Error) -> Action { + if let error = error as? NIOSSLError, error == .uncleanShutdown { + switch self.state { + case .initialized: + preconditionFailure("After the state machine has been initialized, start must be called immediately. Thus this state is unreachable") + + case .modifying: + preconditionFailure("Invalid state: \(self.state)") + + case .running(.streaming, .waitingForHead), + .running(.endSent, .waitingForHead): + // if we received a NIOSSL.uncleanShutdown before we got an answer we should handle + // this like a normal connection close. We will receive a call to channelInactive after + // this error. + return .wait + + case .running(.streaming, .receivingBody(let responseHead, _)), + .running(.endSent, .receivingBody(let responseHead, _)): + // This code is only reachable for request and responses, which we expect to have a body. + // We depend on logic from the HTTPResponseDecoder here. The decoder will emit an + // HTTPResponsePart.end right after the HTTPResponsePart.head, for every request with a + // CONNECT or HEAD method and every response with a 1xx, 204 or 304 response status. + // + // For this reason we only need to check the "content-length" or "transfer-encoding" + // headers here to determine if we are potentially in an EOF terminated response. + + if responseHead.headers.contains(name: "content-length") || responseHead.headers.contains(name: "transfer-encoding") { + // If we have already received the response head, the parser will ensure that we + // receive a complete response, if the content-length or transfer-encoding header + // was set. In this case we can ignore the NIOSSLError.uncleanShutdown. We will see + // a HTTPParserError very soon. + return .wait + } + + // If the response is EOF terminated, we need to rely on a clean tls shutdown to be sure + // we have received all necessary bytes. For this reason we forward the uncleanShutdown + // error to the user. + self.state = .failed(error) + return .failRequest(error, .close) + + case .waitForChannelToBecomeWritable, .running, .finished, .failed: + break + } + } switch self.state { case .initialized: preconditionFailure("After the state machine has been initialized, start must be called immediately. Thus this state is unreachable") @@ -197,38 +240,6 @@ struct HTTPRequestStateMachine { // the request failed, before it was sent onto the wire. self.state = .failed(error) return .failRequest(error, .none) - - case .running(.streaming, .waitingForHead) where error as? NIOSSLError == .uncleanShutdown, - .running(.endSent, .waitingForHead) where error as? NIOSSLError == .uncleanShutdown: - // if we received a NIOSSL.uncleanShutdown before we got an answer we should handle - // this like a normal connection close. We will receive a call to channelInactive after - // this error. - return .wait - - case .running(.streaming, .receivingBody(let responseHead, _)) where error as? NIOSSLError == .uncleanShutdown, - .running(.endSent, .receivingBody(let responseHead, _)) where error as? NIOSSLError == .uncleanShutdown: - // This code is only reachable for request and responses, which we expect to have a body. - // We depend on logic from the HTTPResponseDecoder here. The decoder will emit an - // HTTPResponsePart.end right after the HTTPResponsePart.head, for every request with a - // CONNECT or HEAD method and every response with a 1xx, 204 or 304 response status. - // - // For this reason we only need to check the "content-length" or "transfer-encoding" - // headers here to determine if we are potentially in an EOF terminated response. - - if responseHead.headers.contains(name: "content-length") || responseHead.headers.contains(name: "transfer-encoding") { - // If we have already received the response head, the parser will ensure that we - // receive a complete response, if the content-length or transfer-encoding header - // was set. In this case we can ignore the NIOSSLError.uncleanShutdown. We will see - // a HTTPParserError very soon. - return .wait - } - - // If the response is EOF terminated, we need to rely on a clean tls shutdown to be sure - // we have received all necessary bytes. For this reason we forward the uncleanShutdown - // error to the user. - self.state = .failed(error) - return .failRequest(error, .close) - case .running: self.state = .failed(error) return .failRequest(error, .close) From 2f25c2f00ee48da9e03a8b2872311d534410a3a3 Mon Sep 17 00:00:00 2001 From: David Nadoba <d_nadoba@apple.com> Date: Wed, 24 Nov 2021 15:27:52 +0100 Subject: [PATCH 6/8] move `.uncleanShutdown` handling into seperate function --- .../HTTPRequestStateMachine.swift | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift index 7619b9214..4e23d431e 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift @@ -189,49 +189,49 @@ struct HTTPRequestStateMachine { } } - mutating func errorHappened(_ error: Error) -> Action { - if let error = error as? NIOSSLError, error == .uncleanShutdown { - switch self.state { - case .initialized: - preconditionFailure("After the state machine has been initialized, start must be called immediately. Thus this state is unreachable") - - case .modifying: - preconditionFailure("Invalid state: \(self.state)") - - case .running(.streaming, .waitingForHead), - .running(.endSent, .waitingForHead): - // if we received a NIOSSL.uncleanShutdown before we got an answer we should handle - // this like a normal connection close. We will receive a call to channelInactive after - // this error. + mutating func handleNIOSSLUncleanShutdownError() -> Action? { + switch self.state { + case .running(.streaming, .waitingForHead), + .running(.endSent, .waitingForHead): + // if we received a NIOSSL.uncleanShutdown before we got an answer we should handle + // this like a normal connection close. We will receive a call to channelInactive after + // this error. + return .wait + + case .running(.streaming, .receivingBody(let responseHead, _)), + .running(.endSent, .receivingBody(let responseHead, _)): + // This code is only reachable for request and responses, which we expect to have a body. + // We depend on logic from the HTTPResponseDecoder here. The decoder will emit an + // HTTPResponsePart.end right after the HTTPResponsePart.head, for every request with a + // CONNECT or HEAD method and every response with a 1xx, 204 or 304 response status. + // + // For this reason we only need to check the "content-length" or "transfer-encoding" + // headers here to determine if we are potentially in an EOF terminated response. + + if responseHead.headers.contains(name: "content-length") || responseHead.headers.contains(name: "transfer-encoding") { + // If we have already received the response head, the parser will ensure that we + // receive a complete response, if the content-length or transfer-encoding header + // was set. In this case we can ignore the NIOSSLError.uncleanShutdown. We will see + // a HTTPParserError very soon. return .wait + } - case .running(.streaming, .receivingBody(let responseHead, _)), - .running(.endSent, .receivingBody(let responseHead, _)): - // This code is only reachable for request and responses, which we expect to have a body. - // We depend on logic from the HTTPResponseDecoder here. The decoder will emit an - // HTTPResponsePart.end right after the HTTPResponsePart.head, for every request with a - // CONNECT or HEAD method and every response with a 1xx, 204 or 304 response status. - // - // For this reason we only need to check the "content-length" or "transfer-encoding" - // headers here to determine if we are potentially in an EOF terminated response. - - if responseHead.headers.contains(name: "content-length") || responseHead.headers.contains(name: "transfer-encoding") { - // If we have already received the response head, the parser will ensure that we - // receive a complete response, if the content-length or transfer-encoding header - // was set. In this case we can ignore the NIOSSLError.uncleanShutdown. We will see - // a HTTPParserError very soon. - return .wait - } + // If the response is EOF terminated, we need to rely on a clean tls shutdown to be sure + // we have received all necessary bytes. For this reason we forward the uncleanShutdown + // error to the user. + self.state = .failed(error) + return .failRequest(error, .close) - // If the response is EOF terminated, we need to rely on a clean tls shutdown to be sure - // we have received all necessary bytes. For this reason we forward the uncleanShutdown - // error to the user. - self.state = .failed(error) - return .failRequest(error, .close) + case .waitForChannelToBecomeWritable, .running, .finished, .failed, .initialized, .modifying: + return nil + } + } - case .waitForChannelToBecomeWritable, .running, .finished, .failed: - break - } + mutating func errorHappened(_ error: Error) -> Action { + if let error = error as? NIOSSLError, + error == .uncleanShutdown, + let action = self.handleNIOSSLUncleanShutdownError() { + return action } switch self.state { case .initialized: From 87ab3ca480cb8dec263d11d87bdb828cad981c4a Mon Sep 17 00:00:00 2001 From: David Nadoba <d_nadoba@apple.com> Date: Wed, 24 Nov 2021 15:30:26 +0100 Subject: [PATCH 7/8] make private and move`handleNIOSSLUncleanShutdownError` below `errorHappened` --- .../HTTPRequestStateMachine.swift | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift index 4e23d431e..ad27feac5 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift @@ -189,7 +189,33 @@ struct HTTPRequestStateMachine { } } - mutating func handleNIOSSLUncleanShutdownError() -> Action? { + mutating func errorHappened(_ error: Error) -> Action { + if let error = error as? NIOSSLError, + error == .uncleanShutdown, + let action = self.handleNIOSSLUncleanShutdownError() { + return action + } + switch self.state { + case .initialized: + preconditionFailure("After the state machine has been initialized, start must be called immediately. Thus this state is unreachable") + case .waitForChannelToBecomeWritable: + // the request failed, before it was sent onto the wire. + self.state = .failed(error) + return .failRequest(error, .none) + case .running: + self.state = .failed(error) + return .failRequest(error, .close) + + case .finished, .failed: + // ignore error + return .wait + + case .modifying: + preconditionFailure("Invalid state: \(self.state)") + } + } + + private mutating func handleNIOSSLUncleanShutdownError() -> Action? { switch self.state { case .running(.streaming, .waitingForHead), .running(.endSent, .waitingForHead): @@ -227,32 +253,6 @@ struct HTTPRequestStateMachine { } } - mutating func errorHappened(_ error: Error) -> Action { - if let error = error as? NIOSSLError, - error == .uncleanShutdown, - let action = self.handleNIOSSLUncleanShutdownError() { - return action - } - switch self.state { - case .initialized: - preconditionFailure("After the state machine has been initialized, start must be called immediately. Thus this state is unreachable") - case .waitForChannelToBecomeWritable: - // the request failed, before it was sent onto the wire. - self.state = .failed(error) - return .failRequest(error, .none) - case .running: - self.state = .failed(error) - return .failRequest(error, .close) - - case .finished, .failed: - // ignore error - return .wait - - case .modifying: - preconditionFailure("Invalid state: \(self.state)") - } - } - mutating func requestStreamPartReceived(_ part: IOData) -> Action { switch self.state { case .initialized, From d45d7970e543bba7dbf77eb7bf890de0cf03f404 Mon Sep 17 00:00:00 2001 From: David Nadoba <d_nadoba@apple.com> Date: Wed, 24 Nov 2021 15:32:50 +0100 Subject: [PATCH 8/8] fix compilation --- .../ConnectionPool/HTTPRequestStateMachine.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift index ad27feac5..380f7386e 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift @@ -245,8 +245,8 @@ struct HTTPRequestStateMachine { // If the response is EOF terminated, we need to rely on a clean tls shutdown to be sure // we have received all necessary bytes. For this reason we forward the uncleanShutdown // error to the user. - self.state = .failed(error) - return .failRequest(error, .close) + self.state = .failed(NIOSSLError.uncleanShutdown) + return .failRequest(NIOSSLError.uncleanShutdown, .close) case .waitForChannelToBecomeWritable, .running, .finished, .failed, .initialized, .modifying: return nil