Skip to content

Commit 64fbfda

Browse files
authored
[HTTP1Connection] Option to ignore unclean ssl shutdown errors (#432)
- a new `RequestOptions` struct was created, that can be used to set request specific options. It is required by the `HTTPExecutableRequest` - Added support for `ignoreUncleanSSLShutdown` in the `HTTPRequestStateMachine` and the `HTTP1ConnectionStateMachine`. In http/2 `ignoreUncleanSSLShutdown` is always off.
1 parent b25943a commit 64fbfda

20 files changed

+219
-89
lines changed

Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ClientChannelHandler.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
3939
requestLogger[metadataKey: "ahc-el"] = "\(self.connection.channel.eventLoop)"
4040
self.logger = requestLogger
4141

42-
if let idleReadTimeout = newRequest.idleReadTimeout {
42+
if let idleReadTimeout = newRequest.requestOptions.idleReadTimeout {
4343
self.idleReadTimeoutStateMachine = .init(timeAmount: idleReadTimeout)
4444
}
4545
} else {
@@ -146,7 +146,11 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
146146
self.logger.debug("Request was scheduled on connection")
147147
req.willExecuteRequest(self)
148148

149-
let action = self.state.runNewRequest(head: req.requestHead, metadata: req.requestFramingMetadata)
149+
let action = self.state.runNewRequest(
150+
head: req.requestHead,
151+
metadata: req.requestFramingMetadata,
152+
ignoreUncleanSSLShutdown: req.requestOptions.ignoreUncleanSSLShutdown
153+
)
150154
self.run(action, context: context)
151155
}
152156

Sources/AsyncHTTPClient/ConnectionPool/HTTP1.1/HTTP1ConnectionStateMachine.swift

+7-2
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,18 @@ struct HTTP1ConnectionStateMachine {
154154
}
155155
}
156156

157-
mutating func runNewRequest(head: HTTPRequestHead, metadata: RequestFramingMetadata) -> Action {
157+
mutating func runNewRequest(
158+
head: HTTPRequestHead,
159+
metadata: RequestFramingMetadata,
160+
ignoreUncleanSSLShutdown: Bool
161+
) -> Action {
158162
guard case .idle = self.state else {
159163
preconditionFailure("Invalid state")
160164
}
161165

162166
var requestStateMachine = HTTPRequestStateMachine(
163-
isChannelWritable: self.isChannelWritable
167+
isChannelWritable: self.isChannelWritable,
168+
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown
164169
)
165170
let action = requestStateMachine.startRequest(head: head, metadata: metadata)
166171

Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
2424

2525
private let eventLoop: EventLoop
2626

27-
private var state: HTTPRequestStateMachine = .init(isChannelWritable: false) {
27+
private var state: HTTPRequestStateMachine = .init(isChannelWritable: false, ignoreUncleanSSLShutdown: false) {
2828
willSet {
2929
self.eventLoop.assertInEventLoop()
3030
}
@@ -35,7 +35,7 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
3535

3636
private var request: HTTPExecutableRequest? {
3737
didSet {
38-
if let newRequest = self.request, let idleReadTimeout = newRequest.idleReadTimeout {
38+
if let newRequest = self.request, let idleReadTimeout = newRequest.requestOptions.idleReadTimeout {
3939
self.idleReadTimeoutStateMachine = .init(timeAmount: idleReadTimeout)
4040
} else {
4141
self.idleReadTimeoutStateMachine = nil

Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutableRequest.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ protocol HTTPExecutableRequest: AnyObject {
219219
/// ``requestHeadSent``.
220220
var requestFramingMetadata: RequestFramingMetadata { get }
221221

222-
/// The maximal `TimeAmount` that is allowed to pass between `channelRead`s from the Channel.
223-
var idleReadTimeout: TimeAmount? { get }
222+
/// Request specific configurations
223+
var requestOptions: RequestOptions { get }
224224

225225
/// Will be called by the ChannelHandler to indicate that the request is going to be sent.
226226
///

Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import NIOCore
1616
import NIOHTTP1
17+
import NIOSSL
1718

1819
struct HTTPRequestStateMachine {
1920
fileprivate enum State {
@@ -102,8 +103,11 @@ struct HTTPRequestStateMachine {
102103

103104
private var isChannelWritable: Bool
104105

105-
init(isChannelWritable: Bool) {
106+
private let ignoreUncleanSSLShutdown: Bool
107+
108+
init(isChannelWritable: Bool, ignoreUncleanSSLShutdown: Bool) {
106109
self.isChannelWritable = isChannelWritable
110+
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
107111
}
108112

109113
mutating func startRequest(head: HTTPRequestHead, metadata: RequestFramingMetadata) -> Action {
@@ -196,6 +200,12 @@ struct HTTPRequestStateMachine {
196200
// the request failed, before it was sent onto the wire.
197201
self.state = .failed(error)
198202
return .failRequest(error, .none)
203+
204+
case .running(.streaming, .receivingBody),
205+
.running(.endSent, .receivingBody)
206+
where error as? NIOSSLError == .uncleanShutdown && self.ignoreUncleanSSLShutdown == true:
207+
return .wait
208+
199209
case .running:
200210
self.state = .failed(error)
201211
return .failRequest(error, .close)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
17+
struct RequestOptions {
18+
/// The maximal `TimeAmount` that is allowed to pass between `channelRead`s from the Channel.
19+
var idleReadTimeout: TimeAmount?
20+
21+
/// Should `NIOSSLError.uncleanShutdown` be forwarded to the user in HTTP/1 mode.
22+
var ignoreUncleanSSLShutdown: Bool
23+
24+
init(idleReadTimeout: TimeAmount?, ignoreUncleanSSLShutdown: Bool) {
25+
self.idleReadTimeout = idleReadTimeout
26+
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
27+
}
28+
}
29+
30+
extension RequestOptions {
31+
static func fromClientConfiguration(_ configuration: HTTPClient.Configuration) -> Self {
32+
RequestOptions(
33+
idleReadTimeout: configuration.timeout.read,
34+
ignoreUncleanSSLShutdown: configuration.ignoreUncleanSSLShutdown
35+
)
36+
}
37+
}

Sources/AsyncHTTPClient/RequestBag.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ final class RequestBag<Delegate: HTTPClientResponseDelegate> {
3939

4040
let connectionDeadline: NIODeadline
4141

42-
let idleReadTimeout: TimeAmount?
42+
let requestOptions: RequestOptions
4343

4444
let requestHead: HTTPRequestHead
4545
let requestFramingMetadata: RequestFramingMetadata
@@ -51,14 +51,14 @@ final class RequestBag<Delegate: HTTPClientResponseDelegate> {
5151
task: HTTPClient.Task<Delegate.Response>,
5252
redirectHandler: RedirectHandler<Delegate.Response>?,
5353
connectionDeadline: NIODeadline,
54-
idleReadTimeout: TimeAmount?,
54+
requestOptions: RequestOptions,
5555
delegate: Delegate) throws {
5656
self.eventLoopPreference = eventLoopPreference
5757
self.task = task
5858
self.state = .init(redirectHandler: redirectHandler)
5959
self.request = request
6060
self.connectionDeadline = connectionDeadline
61-
self.idleReadTimeout = idleReadTimeout
61+
self.requestOptions = requestOptions
6262
self.delegate = delegate
6363

6464
let (head, metadata) = try request.createRequestHead()

Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
3838
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
3939
redirectHandler: nil,
4040
connectionDeadline: .now() + .seconds(30),
41-
idleReadTimeout: nil,
41+
requestOptions: .forTests(),
4242
delegate: delegate
4343
))
4444
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -126,7 +126,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
126126
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
127127
redirectHandler: nil,
128128
connectionDeadline: .now() + .seconds(30),
129-
idleReadTimeout: .milliseconds(200),
129+
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
130130
delegate: delegate
131131
))
132132
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -207,7 +207,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
207207
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
208208
redirectHandler: nil,
209209
connectionDeadline: .now() + .seconds(30),
210-
idleReadTimeout: .milliseconds(200),
210+
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
211211
delegate: delegate
212212
))
213213
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -253,7 +253,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
253253
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
254254
redirectHandler: nil,
255255
connectionDeadline: .now() + .seconds(30),
256-
idleReadTimeout: .milliseconds(200),
256+
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
257257
delegate: delegate
258258
))
259259
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }

Tests/AsyncHTTPClientTests/HTTP1ConnectionStateMachineTests.swift

+17-10
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
2525

2626
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["content-length": "4"])
2727
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
28-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
28+
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false), .wait)
2929
XCTAssertEqual(state.writabilityChanged(writable: true), .sendRequestHead(requestHead, startBody: true))
3030

3131
let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0]))
@@ -63,7 +63,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
6363

6464
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
6565
let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
66-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
66+
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
67+
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
6768

6869
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["content-length": "12"])
6970
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -90,7 +91,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
9091
XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
9192
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: ["connection": "close"])
9293
let metadata = RequestFramingMetadata(connectionClose: true, body: .none)
93-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
94+
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
95+
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
9496

9597
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
9698
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -105,7 +107,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
105107
XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
106108
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
107109
let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
108-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
110+
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
111+
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
109112

110113
let responseHead = HTTPResponseHead(version: .http1_0, status: .ok, headers: ["content-length": "4"])
111114
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -120,7 +123,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
120123
XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
121124
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
122125
let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
123-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
126+
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
127+
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
124128

125129
let responseHead = HTTPResponseHead(version: .http1_0, status: .ok, headers: ["content-length": "4", "connection": "keep-alive"])
126130
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -136,7 +140,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
136140
XCTAssertEqual(state.writabilityChanged(writable: true), .wait)
137141
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
138142
let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
139-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
143+
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
144+
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
140145

141146
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["connection": "close"])
142147
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
@@ -164,7 +169,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
164169

165170
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
166171
let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
167-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
172+
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
173+
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
168174

169175
XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none))
170176
}
@@ -175,7 +181,7 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
175181

176182
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["content-length": "4"])
177183
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
178-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
184+
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false), .wait)
179185
XCTAssertEqual(state.writabilityChanged(writable: true), .sendRequestHead(requestHead, startBody: true))
180186

181187
let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0]))
@@ -219,7 +225,7 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
219225
XCTAssertEqual(state.channelActive(isWritable: false), .fireChannelActive)
220226
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["content-length": "4"])
221227
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
222-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
228+
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false), .wait)
223229
XCTAssertEqual(state.requestCancelled(closeConnection: false), .failRequest(HTTPClientError.cancelled, .informConnectionIsIdle))
224230
}
225231

@@ -228,7 +234,8 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
228234
XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
229235
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
230236
let metadata = RequestFramingMetadata(connectionClose: false, body: .none)
231-
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, startBody: false))
237+
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata, ignoreUncleanSSLShutdown: false)
238+
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, startBody: false))
232239
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
233240
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
234241
XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: "Hello world!\n"))), .wait)

Tests/AsyncHTTPClientTests/HTTP1ConnectionTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ class HTTP1ConnectionTests: XCTestCase {
151151
task: task,
152152
redirectHandler: nil,
153153
connectionDeadline: .now() + .seconds(60),
154-
idleReadTimeout: nil,
154+
requestOptions: .forTests(),
155155
delegate: ResponseAccumulator(request: request)
156156
))
157157
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
@@ -223,7 +223,7 @@ class HTTP1ConnectionTests: XCTestCase {
223223
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
224224
redirectHandler: nil,
225225
connectionDeadline: .now() + .seconds(30),
226-
idleReadTimeout: nil,
226+
requestOptions: .forTests(),
227227
delegate: delegate
228228
))
229229
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -281,7 +281,7 @@ class HTTP1ConnectionTests: XCTestCase {
281281
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
282282
redirectHandler: nil,
283283
connectionDeadline: .now() + .seconds(30),
284-
idleReadTimeout: nil,
284+
requestOptions: .forTests(),
285285
delegate: delegate
286286
))
287287
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
@@ -348,7 +348,7 @@ class HTTP1ConnectionTests: XCTestCase {
348348
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
349349
redirectHandler: nil,
350350
connectionDeadline: .now() + .seconds(30),
351-
idleReadTimeout: nil,
351+
requestOptions: .forTests(),
352352
delegate: delegate
353353
))
354354
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }

0 commit comments

Comments
 (0)