Skip to content

Commit 4c5733d

Browse files
committed
throw connection error if deadline is exceeded
1 parent b3583ba commit 4c5733d

14 files changed

+145
-46
lines changed

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,10 @@ extension HTTPConnectionPool {
323323

324324
mutating func cancelRequest(_ requestID: Request.ID) -> Action {
325325
// 1. check requests in queue
326-
if self.requests.remove(requestID) != nil {
326+
if let request = self.requests.remove(requestID) {
327+
let error = lastConnectFailure ?? HTTPClientError.cancelled
327328
return .init(
328-
request: .cancelRequestTimeout(requestID),
329+
request: .failRequest(request, error, cancelTimeout: true),
329330
connection: .none
330331
)
331332
}

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP2StateMachine.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -444,9 +444,10 @@ extension HTTPConnectionPool {
444444

445445
mutating func cancelRequest(_ requestID: Request.ID) -> Action {
446446
// 1. check requests in queue
447-
if self.requests.remove(requestID) != nil {
447+
if let request = self.requests.remove(requestID) {
448+
let error = lastConnectFailure ?? HTTPClientError.cancelled
448449
return .init(
449-
request: .cancelRequestTimeout(requestID),
450+
request: .failRequest(request, error, cancelTimeout: true),
450451
connection: .none
451452
)
452453
}

Sources/AsyncHTTPClient/HTTPClient.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ public class HTTPClient {
606606
var deadlineSchedule: Scheduled<Void>?
607607
if let deadline = deadline {
608608
deadlineSchedule = taskEL.scheduleTask(deadline: deadline) {
609-
requestBag.fail(HTTPClientError.deadlineExceeded)
609+
requestBag.cancel(.deadlineExceeded)
610610
}
611611

612612
task.promise.futureResult.whenComplete { _ in

Sources/AsyncHTTPClient/HTTPHandler.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ extension URL {
556556
}
557557

558558
protocol HTTPClientTaskDelegate {
559-
func cancel()
559+
func cancel(_ reason: CancelationReason)
560560
}
561561

562562
extension HTTPClient {
@@ -618,7 +618,7 @@ extension HTTPClient {
618618
return self._taskDelegate
619619
}
620620

621-
taskDelegate?.cancel()
621+
taskDelegate?.cancel(.userInitiated)
622622
}
623623

624624
func succeed<Delegate: HTTPClientResponseDelegate>(promise: EventLoopPromise<Response>?,

Sources/AsyncHTTPClient/RequestBag+StateMachine.swift

+41-17
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ extension RequestBag {
3131
fileprivate enum State {
3232
case initialized
3333
case queued(HTTPRequestScheduler)
34+
case canceledWhileQueued(CancelationReason)
3435
case executing(HTTPRequestExecutor, RequestStreamState, ResponseStreamState)
3536
case finished(error: Error?)
3637
case redirected(HTTPRequestExecutor, Int, HTTPResponseHead, URL)
@@ -95,7 +96,7 @@ extension RequestBag.StateMachine {
9596
case .initialized, .queued:
9697
self.state = .executing(executor, .initialized, .initialized)
9798
return true
98-
case .finished(error: .some):
99+
case .finished(error: .some), .canceledWhileQueued:
99100
return false
100101
case .executing, .redirected, .finished(error: .none), .modifying:
101102
preconditionFailure("Invalid state: \(self.state)")
@@ -110,7 +111,7 @@ extension RequestBag.StateMachine {
110111

111112
mutating func resumeRequestBodyStream() -> ResumeProducingAction {
112113
switch self.state {
113-
case .initialized, .queued:
114+
case .initialized, .queued, .canceledWhileQueued:
114115
preconditionFailure("A request stream can only be resumed, if the request was started")
115116

116117
case .executing(let executor, .initialized, .initialized):
@@ -150,7 +151,7 @@ extension RequestBag.StateMachine {
150151

151152
mutating func pauseRequestBodyStream() {
152153
switch self.state {
153-
case .initialized, .queued:
154+
case .initialized, .queued, .canceledWhileQueued:
154155
preconditionFailure("A request stream can only be paused, if the request was started")
155156
case .executing(let executor, let requestState, let responseState):
156157
switch requestState {
@@ -185,7 +186,7 @@ extension RequestBag.StateMachine {
185186

186187
mutating func writeNextRequestPart(_ part: IOData, taskEventLoop: EventLoop) -> WriteAction {
187188
switch self.state {
188-
case .initialized, .queued:
189+
case .initialized, .queued, .canceledWhileQueued:
189190
preconditionFailure("Invalid state: \(self.state)")
190191
case .executing(let executor, let requestState, let responseState):
191192
switch requestState {
@@ -231,7 +232,7 @@ extension RequestBag.StateMachine {
231232

232233
mutating func finishRequestBodyStream(_ result: Result<Void, Error>) -> FinishAction {
233234
switch self.state {
234-
case .initialized, .queued:
235+
case .initialized, .queued, .canceledWhileQueued:
235236
preconditionFailure("Invalid state: \(self.state)")
236237
case .executing(let executor, let requestState, let responseState):
237238
switch requestState {
@@ -282,7 +283,7 @@ extension RequestBag.StateMachine {
282283
/// - Returns: Whether the response should be forwarded to the delegate. Will be `false` if the request follows a redirect.
283284
mutating func receiveResponseHead(_ head: HTTPResponseHead) -> ReceiveResponseHeadAction {
284285
switch self.state {
285-
case .initialized, .queued:
286+
case .initialized, .queued, .canceledWhileQueued:
286287
preconditionFailure("How can we receive a response, if the request hasn't started yet.")
287288
case .executing(let executor, let requestState, let responseState):
288289
guard case .initialized = responseState else {
@@ -328,7 +329,7 @@ extension RequestBag.StateMachine {
328329

329330
mutating func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>) -> ReceiveResponseBodyAction {
330331
switch self.state {
331-
case .initialized, .queued:
332+
case .initialized, .queued, .canceledWhileQueued:
332333
preconditionFailure("How can we receive a response body part, if the request hasn't started yet.")
333334
case .executing(_, _, .initialized):
334335
preconditionFailure("If we receive a response body, we must have received a head before")
@@ -385,7 +386,7 @@ extension RequestBag.StateMachine {
385386

386387
mutating func succeedRequest(_ newChunks: CircularBuffer<ByteBuffer>?) -> ReceiveResponseEndAction {
387388
switch self.state {
388-
case .initialized, .queued:
389+
case .initialized, .queued, .canceledWhileQueued:
389390
preconditionFailure("How can we receive a response body part, if the request hasn't started yet.")
390391
case .executing(_, _, .initialized):
391392
preconditionFailure("If we receive a response body, we must have received a head before")
@@ -447,7 +448,7 @@ extension RequestBag.StateMachine {
447448

448449
private mutating func failWithConsumptionError(_ error: Error) -> ConsumeAction {
449450
switch self.state {
450-
case .initialized, .queued:
451+
case .initialized, .queued, .canceledWhileQueued:
451452
preconditionFailure("Invalid state: \(self.state)")
452453
case .executing(_, _, .initialized):
453454
preconditionFailure("Invalid state: Must have received response head, before this method is called for the first time")
@@ -482,7 +483,7 @@ extension RequestBag.StateMachine {
482483

483484
private mutating func consumeMoreBodyData() -> ConsumeAction {
484485
switch self.state {
485-
case .initialized, .queued:
486+
case .initialized, .queued, .canceledWhileQueued:
486487
preconditionFailure("Invalid state: \(self.state)")
487488

488489
case .executing(_, _, .initialized):
@@ -531,9 +532,24 @@ extension RequestBag.StateMachine {
531532
preconditionFailure()
532533
}
533534
}
535+
536+
enum CancelAction {
537+
case cancelScheduler(HTTPRequestScheduler?)
538+
case fail(FailAction)
539+
}
540+
541+
mutating func cancel(_ reason: CancelationReason) -> CancelAction {
542+
switch self.state {
543+
case .queued(let queuer) where reason == .deadlineExceeded:
544+
self.state = .canceledWhileQueued(reason)
545+
return .cancelScheduler(queuer)
546+
default:
547+
return .fail(self.fail(reason.error))
548+
}
549+
}
534550

535551
enum FailAction {
536-
case failTask(HTTPRequestScheduler?, HTTPRequestExecutor?)
552+
case failTask(Error, HTTPRequestScheduler?, HTTPRequestExecutor?)
537553
case cancelExecutor(HTTPRequestExecutor)
538554
case none
539555
}
@@ -542,31 +558,39 @@ extension RequestBag.StateMachine {
542558
switch self.state {
543559
case .initialized:
544560
self.state = .finished(error: error)
545-
return .failTask(nil, nil)
561+
return .failTask(error, nil, nil)
546562
case .queued(let queuer):
547563
self.state = .finished(error: error)
548-
return .failTask(queuer, nil)
564+
return .failTask(error, queuer, nil)
549565
case .executing(let executor, let requestState, .buffering(_, next: .eof)):
550566
self.state = .executing(executor, requestState, .buffering(.init(), next: .error(error)))
551567
return .cancelExecutor(executor)
552568
case .executing(let executor, _, .buffering(_, next: .askExecutorForMore)):
553569
self.state = .finished(error: error)
554-
return .failTask(nil, executor)
570+
return .failTask(error, nil, executor)
555571
case .executing(let executor, _, .buffering(_, next: .error(_))):
556572
// this would override another error, let's keep the first one
557573
return .cancelExecutor(executor)
558574
case .executing(let executor, _, .initialized):
559575
self.state = .finished(error: error)
560-
return .failTask(nil, executor)
576+
return .failTask(error, nil, executor)
561577
case .executing(let executor, _, .waitingForRemote):
562578
self.state = .finished(error: error)
563-
return .failTask(nil, executor)
579+
return .failTask(error, nil, executor)
564580
case .redirected:
565581
self.state = .finished(error: error)
566-
return .failTask(nil, nil)
582+
return .failTask(error, nil, nil)
567583
case .finished(.none):
568584
// An error occurred after the request has finished. Ignore...
569585
return .none
586+
case .canceledWhileQueued(let reason):
587+
// if we just get a `HTTPClientError.cancelled` we can use the orignal cancelation reason
588+
// to give a more descriptive error to the user.
589+
if (error as? HTTPClientError) == .cancelled {
590+
return .failTask(reason.error, nil, nil)
591+
}
592+
// otherwise we already had an intermidate connection error which we should present to the user instead
593+
return .failTask(error, nil, nil)
570594
case .finished(.some(_)):
571595
// this might happen, if the stream consumer has failed... let's just drop the data
572596
return .none

Sources/AsyncHTTPClient/RequestBag.swift

+35-4
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,12 @@ final class RequestBag<Delegate: HTTPClientResponseDelegate> {
320320

321321
let action = self.state.fail(error)
322322

323+
self.executeFailAction0(action)
324+
}
325+
326+
private func executeFailAction0(_ action: RequestBag<Delegate>.StateMachine.FailAction) {
323327
switch action {
324-
case .failTask(let scheduler, let executor):
328+
case .failTask(let error, let scheduler, let executor):
325329
scheduler?.cancelRequest(self)
326330
executor?.cancelRequest(self)
327331
self.failTask0(error)
@@ -331,6 +335,33 @@ final class RequestBag<Delegate: HTTPClientResponseDelegate> {
331335
break
332336
}
333337
}
338+
339+
private func cancel0(_ reason: CancelationReason) {
340+
self.task.eventLoop.assertInEventLoop()
341+
342+
let action = self.state.cancel(reason)
343+
344+
switch action {
345+
case .cancelScheduler(let scheduler):
346+
scheduler?.cancelRequest(self)
347+
case .fail(let failAction):
348+
self.executeFailAction0(failAction)
349+
}
350+
}
351+
}
352+
353+
enum CancelationReason {
354+
case userInitiated
355+
case deadlineExceeded
356+
357+
var error: HTTPClientError {
358+
switch self {
359+
case .userInitiated:
360+
return .cancelled
361+
case .deadlineExceeded:
362+
return .deadlineExceeded
363+
}
364+
}
334365
}
335366

336367
extension RequestBag: HTTPSchedulableRequest {
@@ -456,12 +487,12 @@ extension RequestBag: HTTPExecutableRequest {
456487
}
457488

458489
extension RequestBag: HTTPClientTaskDelegate {
459-
func cancel() {
490+
func cancel(_ reason: CancelationReason) {
460491
if self.task.eventLoop.inEventLoop {
461-
self.fail0(HTTPClientError.cancelled)
492+
self.cancel0(reason)
462493
} else {
463494
self.task.eventLoop.execute {
464-
self.fail0(HTTPClientError.cancelled)
495+
self.cancel0(reason)
465496
}
466497
}
467498
}

Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
327327
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead)))
328328

329329
// canceling the request
330-
requestBag.cancel()
330+
requestBag.cancel(.userInitiated)
331331
XCTAssertThrowsError(try requestBag.task.futureResult.wait()) {
332332
XCTAssertEqual($0 as? HTTPClientError, .cancelled)
333333
}

Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
276276
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead)))
277277

278278
// canceling the request
279-
requestBag.cancel()
279+
requestBag.cancel(.userInitiated)
280280
XCTAssertThrowsError(try requestBag.task.futureResult.wait()) {
281281
XCTAssertEqual($0 as? HTTPClientError, .cancelled)
282282
}

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+41
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,47 @@ class HTTPClientTests: XCTestCase {
12681268
#endif
12691269
}
12701270
}
1271+
1272+
func testSelfSignedCertificateIsRejectedWithCorrectErrorIfRequestDeadlineIsExceeded() throws {
1273+
/// key + cert was created with the follwing command:
1274+
/// openssl req -x509 -newkey rsa:4096 -keyout self_signed_key.pem -out self_signed_cert.pem -sha256 -days 99999 -nodes -subj '/CN=localhost'
1275+
let certPath = Bundle.module.path(forResource: "self_signed_cert", ofType: "pem")!
1276+
let keyPath = Bundle.module.path(forResource: "self_signed_key", ofType: "pem")!
1277+
let configuration = TLSConfiguration.makeServerConfiguration(
1278+
certificateChain: try NIOSSLCertificate.fromPEMFile(certPath).map { .certificate($0) },
1279+
privateKey: .file(keyPath)
1280+
)
1281+
let sslContext = try NIOSSLContext(configuration: configuration)
1282+
1283+
let server = ServerBootstrap(group: serverGroup)
1284+
.childChannelInitializer { channel in
1285+
channel.pipeline.addHandler(NIOSSLServerHandler(context: sslContext))
1286+
}
1287+
let serverChannel = try server.bind(host: "localhost", port: 0).wait()
1288+
defer { XCTAssertNoThrow(try serverChannel.close().wait()) }
1289+
let port = serverChannel.localAddress!.port!
1290+
1291+
var config = HTTPClient.Configuration()
1292+
config.timeout.connect = .seconds(3)
1293+
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: config)
1294+
defer { XCTAssertNoThrow(try localClient.syncShutdown()) }
1295+
1296+
XCTAssertThrowsError(try localClient.get(url: "https://localhost:\(port)", deadline: .now() + .seconds(2)).wait()) { error in
1297+
#if canImport(Network)
1298+
guard let nwTLSError = error as? HTTPClient.NWTLSError else {
1299+
XCTFail("could not cast \(error) of type \(type(of: error)) to \(HTTPClient.NWTLSError.self)")
1300+
return
1301+
}
1302+
XCTAssertEqual(nwTLSError.status, errSSLBadCert, "unexpected tls error: \(nwTLSError)")
1303+
#else
1304+
guard let sslError = error as? NIOSSLError,
1305+
case .handshakeFailed(.sslError) = sslError else {
1306+
XCTFail("unexpected error \(error)")
1307+
return
1308+
}
1309+
#endif
1310+
}
1311+
}
12711312

12721313
func testFailingConnectionIsReleased() {
12731314
let localHTTPBin = HTTPBin(.refuse)

Tests/AsyncHTTPClientTests/HTTPConnectionPool+HTTP1StateTests.swift

+5-4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import XCTest
2121

2222
class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
2323
func testCreatingAndFailingConnections() {
24+
struct SomeError: Error, Equatable {}
2425
let elg = EmbeddedEventLoopGroup(loops: 4)
2526
defer { XCTAssertNoThrow(try elg.syncShutdownGracefully()) }
2627

@@ -65,7 +66,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
6566

6667
// fail all connection attempts
6768
while let randomConnectionID = connections.randomStartingConnection() {
68-
struct SomeError: Error, Equatable {}
69+
6970

7071
XCTAssertNoThrow(try connections.failConnectionCreation(randomConnectionID))
7172
let action = state.failedToCreateNewConnection(SomeError(), connectionID: randomConnectionID)
@@ -86,9 +87,9 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
8687

8788
// cancel all queued requests
8889
while let request = queuer.timeoutRandomRequest() {
89-
let cancelAction = state.cancelRequest(request)
90+
let cancelAction = state.cancelRequest(request.0)
9091
XCTAssertEqual(cancelAction.connection, .none)
91-
XCTAssertEqual(cancelAction.request, .cancelRequestTimeout(request))
92+
XCTAssertEqual(cancelAction.request, .failRequest(.init(request.1), SomeError(), cancelTimeout: true))
9293
}
9394

9495
// connection backoff done
@@ -184,7 +185,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
184185
// 2. cancel request
185186

186187
let cancelAction = state.cancelRequest(request.id)
187-
XCTAssertEqual(cancelAction.request, .cancelRequestTimeout(request.id))
188+
XCTAssertEqual(cancelAction.request, .failRequest(request, HTTPClientError.cancelled, cancelTimeout: true))
188189
XCTAssertEqual(cancelAction.connection, .none)
189190

190191
// 3. request timeout triggers to late

0 commit comments

Comments
 (0)