From 9b528164fa0ae4646eaea91166819d909b01f569 Mon Sep 17 00:00:00 2001
From: Fabian Fett <fabianfett@apple.com>
Date: Tue, 21 Sep 2021 00:35:36 +0200
Subject: [PATCH 1/2] Add support to ignore NIOSSLError.uncleanShutdown

---
 .../HTTP1.1/HTTP1ClientChannelHandler.swift   |   8 +-
 .../HTTP1.1/HTTP1ConnectionStateMachine.swift |   9 +-
 .../HTTP2/HTTP2ClientRequestHandler.swift     |   4 +-
 .../HTTPExecutableRequest.swift               |   4 +-
 .../HTTPRequestStateMachine.swift             |  12 +-
 .../ConnectionPool/RequestOptions.swift       |  37 ++++++
 Sources/AsyncHTTPClient/RequestBag.swift      |   6 +-
 .../HTTP1ClientChannelHandlerTests.swift      |   8 +-
 .../HTTP1ConnectionStateMachineTests.swift    |  27 +++--
 .../HTTP1ConnectionTests.swift                |   8 +-
 .../HTTP2ClientRequestHandlerTests.swift      |   6 +-
 .../HTTP2ConnectionTests.swift                |   6 +-
 .../HTTPConnectionPool+ManagerTests.swift     |   4 +-
 ...HTTPConnectionPool+RequestQueueTests.swift |   2 +-
 .../HTTPConnectionPoolTests.swift             |  14 +--
 .../HTTPRequestStateMachineTests+XCTest.swift |   3 +
 .../HTTPRequestStateMachineTests.swift        | 108 +++++++++++++-----
 .../Mocks/MockConnectionPool.swift            |   5 +-
 .../RequestBagTests.swift                     |  23 ++--
 19 files changed, 210 insertions(+), 84 deletions(-)
 create mode 100644 Sources/AsyncHTTPClient/ConnectionPool/RequestOptions.swift

diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ClientChannelHandler.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ClientChannelHandler.swift
index 910be3d1d..9c300b5e6 100644
--- a/Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ClientChannelHandler.swift
+++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ClientChannelHandler.swift
@@ -39,7 +39,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
                 requestLogger[metadataKey: "ahc-el"] = "\(self.connection.channel.eventLoop)"
                 self.logger = requestLogger
 
-                if let idleReadTimeout = newRequest.idleReadTimeout {
+                if let idleReadTimeout = newRequest.requestOptions.idleReadTimeout {
                     self.idleReadTimeoutStateMachine = .init(timeAmount: idleReadTimeout)
                 }
             } else {
@@ -146,7 +146,11 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
         self.logger.debug("Request was scheduled on connection")
         req.willExecuteRequest(self)
 
-        let action = self.state.runNewRequest(head: req.requestHead, metadata: req.requestFramingMetadata)
+        let action = self.state.runNewRequest(
+            head: req.requestHead,
+            metadata: req.requestFramingMetadata,
+            ignoreUncleanSSLShutdown: req.requestOptions.ignoreUncleanSSLShutdown
+        )
         self.run(action, context: context)
     }
 
diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ConnectionStateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ConnectionStateMachine.swift
index cebc8f1c1..012544441 100644
--- a/Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ConnectionStateMachine.swift
+++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ConnectionStateMachine.swift
@@ -154,13 +154,18 @@ struct HTTP1ConnectionStateMachine {
         }
     }
 
-    mutating func runNewRequest(head: HTTPRequestHead, metadata: RequestFramingMetadata) -> Action {
+    mutating func runNewRequest(
+        head: HTTPRequestHead,
+        metadata: RequestFramingMetadata,
+        ignoreUncleanSSLShutdown: Bool
+    ) -> Action {
         guard case .idle = self.state else {
             preconditionFailure("Invalid state")
         }
 
         var requestStateMachine = HTTPRequestStateMachine(
-            isChannelWritable: self.isChannelWritable
+            isChannelWritable: self.isChannelWritable,
+            ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown
         )
         let action = requestStateMachine.startRequest(head: head, metadata: metadata)
 
diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
index d17e030fa..281287750 100644
--- a/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
+++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
@@ -24,7 +24,7 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
 
     private let eventLoop: EventLoop
 
-    private var state: HTTPRequestStateMachine = .init(isChannelWritable: false) {
+    private var state: HTTPRequestStateMachine = .init(isChannelWritable: false, ignoreUncleanSSLShutdown: false) {
         willSet {
             self.eventLoop.assertInEventLoop()
         }
@@ -35,7 +35,7 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
 
     private var request: HTTPExecutableRequest? {
         didSet {
-            if let newRequest = self.request, let idleReadTimeout = newRequest.idleReadTimeout {
+            if let newRequest = self.request, let idleReadTimeout = newRequest.requestOptions.idleReadTimeout {
                 self.idleReadTimeoutStateMachine = .init(timeAmount: idleReadTimeout)
             } else {
                 self.idleReadTimeoutStateMachine = nil
diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutableRequest.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutableRequest.swift
index 79aa1c1b3..2477e1154 100644
--- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutableRequest.swift
+++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutableRequest.swift
@@ -219,8 +219,8 @@ protocol HTTPExecutableRequest: AnyObject {
     /// ``requestHeadSent``.
     var requestFramingMetadata: RequestFramingMetadata { get }
 
-    /// The maximal `TimeAmount` that is allowed to pass between `channelRead`s from the Channel.
-    var idleReadTimeout: TimeAmount? { get }
+    /// Request specific configurations
+    var requestOptions: RequestOptions { get }
 
     /// Will be called by the ChannelHandler to indicate that the request is going to be sent.
     ///
diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
index 34d2ec433..b7d05e415 100644
--- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
+++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift
@@ -14,6 +14,7 @@
 
 import NIOCore
 import NIOHTTP1
+import NIOSSL
 
 struct HTTPRequestStateMachine {
     fileprivate enum State {
@@ -102,8 +103,11 @@ struct HTTPRequestStateMachine {
 
     private var isChannelWritable: Bool
 
-    init(isChannelWritable: Bool) {
+    private let ignoreUncleanSSLShutdown: Bool
+
+    init(isChannelWritable: Bool, ignoreUncleanSSLShutdown: Bool) {
         self.isChannelWritable = isChannelWritable
+        self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
     }
 
     mutating func startRequest(head: HTTPRequestHead, metadata: RequestFramingMetadata) -> Action {
@@ -196,6 +200,12 @@ struct HTTPRequestStateMachine {
             // the request failed, before it was sent onto the wire.
             self.state = .failed(error)
             return .failRequest(error, .none)
+
+        case .running(.streaming, .receivingBody),
+             .running(.endSent, .receivingBody)
+                 where error as? NIOSSLError == .uncleanShutdown && self.ignoreUncleanSSLShutdown == true:
+            return .wait
+
         case .running:
             self.state = .failed(error)
             return .failRequest(error, .close)
diff --git a/Sources/AsyncHTTPClient/ConnectionPool/RequestOptions.swift b/Sources/AsyncHTTPClient/ConnectionPool/RequestOptions.swift
new file mode 100644
index 000000000..98f92b661
--- /dev/null
+++ b/Sources/AsyncHTTPClient/ConnectionPool/RequestOptions.swift
@@ -0,0 +1,37 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the AsyncHTTPClient open source project
+//
+// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIOCore
+
+struct RequestOptions {
+    /// The maximal `TimeAmount` that is allowed to pass between `channelRead`s from the Channel.
+    var idleReadTimeout: TimeAmount?
+
+    /// Should `NIOSSLError.uncleanShutdown` be forwarded to the user in HTTP/1 mode.
+    var ignoreUncleanSSLShutdown: Bool
+
+    init(idleReadTimeout: TimeAmount?, ignoreUncleanSSLShutdown: Bool) {
+        self.idleReadTimeout = idleReadTimeout
+        self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
+    }
+}
+
+extension RequestOptions {
+    static func fromClientConfiguration(_ configuration: HTTPClient.Configuration) -> Self {
+        RequestOptions(
+            idleReadTimeout: configuration.timeout.read,
+            ignoreUncleanSSLShutdown: configuration.ignoreUncleanSSLShutdown
+        )
+    }
+}
diff --git a/Sources/AsyncHTTPClient/RequestBag.swift b/Sources/AsyncHTTPClient/RequestBag.swift
index 1be6ba9d2..939f46ee2 100644
--- a/Sources/AsyncHTTPClient/RequestBag.swift
+++ b/Sources/AsyncHTTPClient/RequestBag.swift
@@ -39,7 +39,7 @@ final class RequestBag<Delegate: HTTPClientResponseDelegate> {
 
     let connectionDeadline: NIODeadline
 
-    let idleReadTimeout: TimeAmount?
+    let requestOptions: RequestOptions
 
     let requestHead: HTTPRequestHead
     let requestFramingMetadata: RequestFramingMetadata
@@ -51,14 +51,14 @@ final class RequestBag<Delegate: HTTPClientResponseDelegate> {
          task: HTTPClient.Task<Delegate.Response>,
          redirectHandler: RedirectHandler<Delegate.Response>?,
          connectionDeadline: NIODeadline,
-         idleReadTimeout: TimeAmount?,
+         requestOptions: RequestOptions,
          delegate: Delegate) throws {
         self.eventLoopPreference = eventLoopPreference
         self.task = task
         self.state = .init(redirectHandler: redirectHandler)
         self.request = request
         self.connectionDeadline = connectionDeadline
-        self.idleReadTimeout = idleReadTimeout
+        self.requestOptions = requestOptions
         self.delegate = delegate
 
         let (head, metadata) = try request.createRequestHead()
diff --git a/Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift b/Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift
index 5fa41d7a5..025c18faf 100644
--- a/Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift
@@ -38,7 +38,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
             task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -126,7 +126,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
             task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: .milliseconds(200),
+            requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -207,7 +207,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
             task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: .milliseconds(200),
+            requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -253,7 +253,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
             task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: .milliseconds(200),
+            requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
diff --git a/Tests/AsyncHTTPClientTests/HTTP1ConnectionStateMachineTests.swift b/Tests/AsyncHTTPClientTests/HTTP1ConnectionStateMachineTests.swift
index 11fac1d04..d3e6a37a4 100644
--- a/Tests/AsyncHTTPClientTests/HTTP1ConnectionStateMachineTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTP1ConnectionStateMachineTests.swift
@@ -25,7 +25,7 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
 
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["content-length": "4"])
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
+        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false), .wait)
         XCTAssertEqual(state.writabilityChanged(writable: true), .sendRequestHead(requestHead, startBody: true))
 
         let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0]))
@@ -63,7 +63,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
 
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+        let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
+        XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
 
         let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["content-length": "12"])
         XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -90,7 +91,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
         XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: ["connection": "close"])
         let metadata = RequestFramingMetadata(connectionClose: true, body: .none)
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+        let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
+        XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
 
         let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
         XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -105,7 +107,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
         XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+        let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
+        XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
 
         let responseHead = HTTPResponseHead(version: .http1_0, status: .ok, headers: ["content-length": "4"])
         XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -120,7 +123,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
         XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+        let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
+        XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
 
         let responseHead = HTTPResponseHead(version: .http1_0, status: .ok, headers: ["content-length": "4", "connection": "keep-alive"])
         XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -136,7 +140,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
         XCTAssertEqual(state.writabilityChanged(writable: true), .wait)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+        let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
+        XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
 
         let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["connection": "close"])
         XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -164,7 +169,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
 
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+        let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
+        XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
 
         XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none))
     }
@@ -175,7 +181,7 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
 
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["content-length": "4"])
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
+        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false), .wait)
         XCTAssertEqual(state.writabilityChanged(writable: true), .sendRequestHead(requestHead, startBody: true))
 
         let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0]))
@@ -219,7 +225,7 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
         XCTAssertEqual(state.channelActive(isWritable: false), .fireChannelActive)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["content-length": "4"])
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
+        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false), .wait)
         XCTAssertEqual(state.requestCancelled(closeConnection: false), .failRequest(HTTPClientError.cancelled, .informConnectionIsIdle))
     }
 
@@ -228,7 +234,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
         XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
-        XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+        let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
+        XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
         let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
         XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
         XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: "Hello world!\n"))), .wait)
diff --git a/Tests/AsyncHTTPClientTests/HTTP1ConnectionTests.swift b/Tests/AsyncHTTPClientTests/HTTP1ConnectionTests.swift
index 9881707aa..bb99454d6 100644
--- a/Tests/AsyncHTTPClientTests/HTTP1ConnectionTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTP1ConnectionTests.swift
@@ -151,7 +151,7 @@ class HTTP1ConnectionTests: XCTestCase {
             task: task,
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(60),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: ResponseAccumulator(request: request)
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -223,7 +223,7 @@ class HTTP1ConnectionTests: XCTestCase {
             task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -281,7 +281,7 @@ class HTTP1ConnectionTests: XCTestCase {
                 task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
                 redirectHandler: nil,
                 connectionDeadline: .now() + .seconds(30),
-                idleReadTimeout: nil,
+                requestOptions: .forTests(),
                 delegate: delegate
             ))
             guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -348,7 +348,7 @@ class HTTP1ConnectionTests: XCTestCase {
             task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
diff --git a/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift b/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift
index 44f0adc3b..c1fbced67 100644
--- a/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift
@@ -40,7 +40,7 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
             task: .init(eventLoop: embedded.eventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -128,7 +128,7 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
             task: .init(eventLoop: embedded.eventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: .milliseconds(200),
+            requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -204,7 +204,7 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
             task: .init(eventLoop: embedded.eventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: .milliseconds(200),
+            requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
             delegate: delegate
         ))
         guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
diff --git a/Tests/AsyncHTTPClientTests/HTTP2ConnectionTests.swift b/Tests/AsyncHTTPClientTests/HTTP2ConnectionTests.swift
index c4d5f926f..d940f01f6 100644
--- a/Tests/AsyncHTTPClientTests/HTTP2ConnectionTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTP2ConnectionTests.swift
@@ -72,7 +72,7 @@ class HTTP2ConnectionTests: XCTestCase {
             task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
             redirectHandler: nil,
             connectionDeadline: .distantFuture,
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
         ))
         guard let requestBag = maybeRequestBag else {
@@ -133,7 +133,7 @@ class HTTP2ConnectionTests: XCTestCase {
                 task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
                 redirectHandler: nil,
                 connectionDeadline: .distantFuture,
-                idleReadTimeout: nil,
+                requestOptions: .forTests(),
                 delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
             ))
             guard let requestBag = maybeRequestBag else {
@@ -195,7 +195,7 @@ class HTTP2ConnectionTests: XCTestCase {
                 task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
                 redirectHandler: nil,
                 connectionDeadline: .distantFuture,
-                idleReadTimeout: nil,
+                requestOptions: .forTests(),
                 delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
             ))
             guard let requestBag = maybeRequestBag else {
diff --git a/Tests/AsyncHTTPClientTests/HTTPConnectionPool+ManagerTests.swift b/Tests/AsyncHTTPClientTests/HTTPConnectionPool+ManagerTests.swift
index c4669455e..10f5d92d0 100644
--- a/Tests/AsyncHTTPClientTests/HTTPConnectionPool+ManagerTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTPConnectionPool+ManagerTests.swift
@@ -54,7 +54,7 @@ class HTTPConnectionPool_ManagerTests: XCTestCase {
                 task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
                 redirectHandler: nil,
                 connectionDeadline: .now() + .seconds(5),
-                idleReadTimeout: nil,
+                requestOptions: .forTests(),
                 delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
             ))
 
@@ -110,7 +110,7 @@ class HTTPConnectionPool_ManagerTests: XCTestCase {
             task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(5),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
         ))
 
diff --git a/Tests/AsyncHTTPClientTests/HTTPConnectionPool+RequestQueueTests.swift b/Tests/AsyncHTTPClientTests/HTTPConnectionPool+RequestQueueTests.swift
index a545e06ea..f8d6044cd 100644
--- a/Tests/AsyncHTTPClientTests/HTTPConnectionPool+RequestQueueTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTPConnectionPool+RequestQueueTests.swift
@@ -107,7 +107,7 @@ private class MockScheduledRequest: HTTPSchedulableRequest {
 
     var requestHead: HTTPRequestHead { preconditionFailure("Unimplemented") }
     var requestFramingMetadata: RequestFramingMetadata { preconditionFailure("Unimplemented") }
-    var idleReadTimeout: TimeAmount? { preconditionFailure("Unimplemented") }
+    var requestOptions: RequestOptions { preconditionFailure("Unimplemented") }
 
     func willExecuteRequest(_: HTTPRequestExecutor) {
         preconditionFailure("Unimplemented")
diff --git a/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift b/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift
index 943b1e489..c0a6e9c87 100644
--- a/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift
@@ -58,7 +58,7 @@ class HTTPConnectionPoolTests: XCTestCase {
                 task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
                 redirectHandler: nil,
                 connectionDeadline: .distantFuture,
-                idleReadTimeout: nil,
+                requestOptions: .forTests(),
                 delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
             ))
 
@@ -112,7 +112,7 @@ class HTTPConnectionPoolTests: XCTestCase {
                 task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
                 redirectHandler: nil,
                 connectionDeadline: .distantFuture,
-                idleReadTimeout: nil,
+                requestOptions: .forTests(),
                 delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
             ))
 
@@ -167,7 +167,7 @@ class HTTPConnectionPoolTests: XCTestCase {
                 task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
                 redirectHandler: nil,
                 connectionDeadline: .distantFuture,
-                idleReadTimeout: nil,
+                requestOptions: .forTests(),
                 delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
             ))
 
@@ -221,7 +221,7 @@ class HTTPConnectionPoolTests: XCTestCase {
             task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(5),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
         ))
 
@@ -269,7 +269,7 @@ class HTTPConnectionPoolTests: XCTestCase {
             task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(5),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
         ))
 
@@ -325,7 +325,7 @@ class HTTPConnectionPoolTests: XCTestCase {
             task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(5),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
         ))
 
@@ -371,7 +371,7 @@ class HTTPConnectionPoolTests: XCTestCase {
             task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(5),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
         ))
 
diff --git a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests+XCTest.swift
index d600092ff..e3b5c72ac 100644
--- a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests+XCTest.swift
+++ b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests+XCTest.swift
@@ -53,6 +53,9 @@ extension HTTPRequestStateMachineTests {
             ("testCanReadHTTP1_0ResponseWithoutBody", testCanReadHTTP1_0ResponseWithoutBody),
             ("testCanReadHTTP1_0ResponseWithBody", testCanReadHTTP1_0ResponseWithBody),
             ("testFailHTTP1_0RequestThatIsStillUploading", testFailHTTP1_0RequestThatIsStillUploading),
+            ("testFailHTTP1RequestWithoutContentLengthWithNIOSSLErrorUncleanShutdown", testFailHTTP1RequestWithoutContentLengthWithNIOSSLErrorUncleanShutdown),
+            ("testFailHTTP1RequestWithoutContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt", testFailHTTP1RequestWithoutContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt),
+            ("testFailHTTP1RequestWithContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt", testFailHTTP1RequestWithContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt),
         ]
     }
 }
diff --git a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift
index 3a11cfd43..2e3eccd77 100644
--- a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift
@@ -20,7 +20,7 @@ import XCTest
 
 class HTTPRequestStateMachineTests: XCTestCase {
     func testSimpleGETRequest() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -34,7 +34,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testPOSTRequestWithWriterBackpressure() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "4")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -68,7 +68,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testPOSTContentLengthIsTooLong() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "4")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -85,7 +85,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testPOSTContentLengthIsTooShort() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "8")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(8))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -101,7 +101,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testRequestBodyStreamIsCancelledIfServerRespondsWith301() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -126,7 +126,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testRequestBodyStreamIsCancelledIfServerRespondsWith301WhileWriteBackpressure() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -151,7 +151,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testRequestBodyStreamIsContinuedIfServerRespondsWith200() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -171,7 +171,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testRequestBodyStreamIsContinuedIfServerSendHeadWithStatus200() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -192,7 +192,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testRequestIsFailedIfRequestBodySizeIsWrongEvenAfterServerRespondedWith200() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -211,7 +211,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testRequestIsFailedIfRequestBodySizeIsWrongEvenAfterServerSendHeadWithStatus200() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
         let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -229,7 +229,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testRequestIsNotSendUntilChannelIsWritable() {
-        var state = HTTPRequestStateMachine(isChannelWritable: false)
+        var state = HTTPRequestStateMachine(isChannelWritable: false, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait)
@@ -245,7 +245,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testConnectionBecomesInactiveWhileWaitingForWritable() {
-        var state = HTTPRequestStateMachine(isChannelWritable: false)
+        var state = HTTPRequestStateMachine(isChannelWritable: false, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait)
@@ -253,7 +253,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testResponseReadingWithBackpressure() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -280,7 +280,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testChannelReadCompleteTriggersButNoBodyDataWasReceivedSoFar() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -307,7 +307,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testResponseReadingWithBackpressureEndOfResponseAllowsReadEventsToTriggerDirectly() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -338,12 +338,12 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testCancellingARequestInStateInitializedKeepsTheConnectionAlive() {
-        var state = HTTPRequestStateMachine(isChannelWritable: false)
+        var state = HTTPRequestStateMachine(isChannelWritable: false, ignoreUncleanSSLShutdown: false)
         XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none))
     }
 
     func testCancellingARequestBeforeBeingSendKeepsTheConnectionAlive() {
-        var state = HTTPRequestStateMachine(isChannelWritable: false)
+        var state = HTTPRequestStateMachine(isChannelWritable: false, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .wait)
@@ -351,7 +351,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testConnectionBecomesWritableBeforeFirstRequest() {
-        var state = HTTPRequestStateMachine(isChannelWritable: false)
+        var state = HTTPRequestStateMachine(isChannelWritable: false, ignoreUncleanSSLShutdown: false)
         XCTAssertEqual(state.writabilityChanged(writable: true), .wait)
 
         // --- sending request
@@ -369,7 +369,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testCancellingARequestThatIsSent() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -377,7 +377,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testRemoteSuddenlyClosesTheConnection() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         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))
@@ -386,7 +386,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testReadTimeoutLeadsToFailureWithEverythingAfterBeingIgnored() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -403,7 +403,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testResponseWithStatus1XXAreIgnored() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -419,7 +419,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testReadTimeoutThatFiresToLateIsIgnored() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -431,7 +431,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testCancellationThatIsInvokedToLateIsIgnored() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -443,7 +443,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testErrorWhileRunningARequestClosesTheStream() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -453,7 +453,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testCanReadHTTP1_0ResponseWithoutBody() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -469,7 +469,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testCanReadHTTP1_0ResponseWithBody() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
@@ -487,7 +487,7 @@ class HTTPRequestStateMachineTests: XCTestCase {
     }
 
     func testFailHTTP1_0RequestThatIsStillUploading() {
-        var state = HTTPRequestStateMachine(isChannelWritable: true)
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
         let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/")
         let metadata = RequestFramingMetadata(connectionClose: false, body: .stream)
         XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: true))
@@ -505,6 +505,58 @@ class HTTPRequestStateMachineTests: XCTestCase {
         XCTAssertEqual(state.channelRead(.end(nil)), .failRequest(HTTPClientError.remoteConnectionClosed, .close))
         XCTAssertEqual(state.channelInactive(), .wait)
     }
+
+    func testFailHTTP1RequestWithoutContentLengthWithNIOSSLErrorUncleanShutdown() {
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: false)
+        let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
+        let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
+        XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+
+        let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
+        let body = ByteBuffer(string: "foo bar")
+        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))
+        XCTAssertEqual(state.channelRead(.end(nil)), .wait)
+        XCTAssertEqual(state.channelInactive(), .wait)
+    }
+
+    func testFailHTTP1RequestWithoutContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt() {
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: true)
+        let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
+        let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
+        XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+
+        let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
+        let body = ByteBuffer(string: "foo bar")
+        XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
+        XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
+        XCTAssertEqual(state.read(), .read)
+        XCTAssertEqual(state.channelRead(.body(body)), .wait)
+        XCTAssertEqual(state.channelReadComplete(), .forwardResponseBodyParts([body]))
+        XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait)
+        XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.close, []))
+        XCTAssertEqual(state.channelInactive(), .wait)
+    }
+
+    func testFailHTTP1RequestWithContentLengthWithNIOSSLErrorUncleanShutdownButIgnoreIt() {
+        var state = HTTPRequestStateMachine(isChannelWritable: true, ignoreUncleanSSLShutdown: true)
+        let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
+        let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
+        XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
+
+        let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["content-length": "30"])
+        let body = ByteBuffer(string: "foo bar")
+        XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
+        XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
+        XCTAssertEqual(state.read(), .read)
+        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))
+        XCTAssertEqual(state.channelInactive(), .wait)
+    }
 }
 
 extension HTTPRequestStateMachine.Action: Equatable {
diff --git a/Tests/AsyncHTTPClientTests/Mocks/MockConnectionPool.swift b/Tests/AsyncHTTPClientTests/Mocks/MockConnectionPool.swift
index 4f3eb9389..9365dd67a 100644
--- a/Tests/AsyncHTTPClientTests/Mocks/MockConnectionPool.swift
+++ b/Tests/AsyncHTTPClientTests/Mocks/MockConnectionPool.swift
@@ -548,7 +548,7 @@ extension MockConnectionPool {
 class MockHTTPRequest: HTTPSchedulableRequest {
     let logger: Logger
     let connectionDeadline: NIODeadline
-    let idleReadTimeout: TimeAmount?
+    let requestOptions: RequestOptions
 
     let preferredEventLoop: EventLoop
     let requiredEventLoop: EventLoop?
@@ -556,12 +556,11 @@ class MockHTTPRequest: HTTPSchedulableRequest {
     init(eventLoop: EventLoop,
          logger: Logger = Logger(label: "mock"),
          connectionTimeout: TimeAmount = .seconds(60),
-         idleReadTimeout: TimeAmount? = nil,
          requiresEventLoopForChannel: Bool = false) {
         self.logger = logger
 
         self.connectionDeadline = .now() + connectionTimeout
-        self.idleReadTimeout = idleReadTimeout
+        self.requestOptions = .forTests()
 
         self.preferredEventLoop = eventLoop
         if requiresEventLoopForChannel {
diff --git a/Tests/AsyncHTTPClientTests/RequestBagTests.swift b/Tests/AsyncHTTPClientTests/RequestBagTests.swift
index 608e81af5..895061e31 100644
--- a/Tests/AsyncHTTPClientTests/RequestBagTests.swift
+++ b/Tests/AsyncHTTPClientTests/RequestBagTests.swift
@@ -63,7 +63,7 @@ final class RequestBagTests: XCTestCase {
             task: .init(eventLoop: embeddedEventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -172,7 +172,7 @@ final class RequestBagTests: XCTestCase {
             task: .init(eventLoop: embeddedEventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -215,7 +215,7 @@ final class RequestBagTests: XCTestCase {
             task: .init(eventLoop: embeddedEventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -248,7 +248,7 @@ final class RequestBagTests: XCTestCase {
             task: .init(eventLoop: embeddedEventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -290,7 +290,7 @@ final class RequestBagTests: XCTestCase {
             task: .init(eventLoop: embeddedEventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -324,7 +324,7 @@ final class RequestBagTests: XCTestCase {
             task: .init(eventLoop: embeddedEventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -381,7 +381,7 @@ final class RequestBagTests: XCTestCase {
             task: .init(eventLoop: embeddedEventLoop, logger: logger),
             redirectHandler: nil,
             connectionDeadline: .now() + .seconds(30),
-            idleReadTimeout: nil,
+            requestOptions: .forTests(),
             delegate: delegate
         ))
         guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -529,3 +529,12 @@ class MockTaskQueuer: HTTPRequestScheduler {
         self.hitCancelCount += 1
     }
 }
+
+extension RequestOptions {
+    static func forTests(idleReadTimeout: TimeAmount? = nil, ignoreUncleanSSLShutdown: Bool = false) -> Self {
+        RequestOptions(
+            idleReadTimeout: idleReadTimeout,
+            ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown
+        )
+    }
+}

From 8d98a872fd429842a421a1ceff9a11bb9eb79a5d Mon Sep 17 00:00:00 2001
From: Fabian Fett <fabianfett@apple.com>
Date: Tue, 21 Sep 2021 00:35:49 +0200
Subject: [PATCH 2/2] Improve test readability

---
 Tests/AsyncHTTPClientTests/HTTPClientTests.swift | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift
index f49bbd0c3..a7b343bc2 100644
--- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift
+++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift
@@ -865,17 +865,21 @@ class HTTPClientTests: XCTestCase {
         guard !isTestingNIOTS() else { return }
 
         let localHTTPBin = HttpBinForSSLUncleanShutdown()
-        let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
-                                     configuration: HTTPClient.Configuration(certificateVerification: .none,
-                                                                             ignoreUncleanSSLShutdown: true))
+        let localClient = HTTPClient(
+            eventLoopGroupProvider: .shared(self.clientGroup),
+            configuration: HTTPClient.Configuration(
+                certificateVerification: .none,
+                ignoreUncleanSSLShutdown: true
+            )
+        )
 
         defer {
             XCTAssertNoThrow(try localClient.syncShutdown())
             localHTTPBin.shutdown()
         }
 
-        XCTAssertThrowsError(try localClient.get(url: "https://localhost:\(localHTTPBin.port)/wrongcontentlength").wait(), "Should fail") { error in
-            XCTAssertEqual(.invalidEOFState, error as? HTTPParserError)
+        XCTAssertThrowsError(try localClient.get(url: "https://localhost:\(localHTTPBin.port)/wrongcontentlength").wait()) {
+            XCTAssertEqual($0 as? HTTPParserError, .invalidEOFState)
         }
     }