Skip to content

Commit f50bf98

Browse files
authored
Tolerate the request stream being started after .finished (#577)
Motivation The RequestBag intermediates between two different threads. This means it can get requests that were reasonable when they were made but have been superseded with newer information since then. These generally have to be tolerated. Unfortunately if we received a request to resume the request body stream _after_ the need for that stream has been invalidated, we could hit a crash. That's unnecessary, and we should tolerate it better. Modifications Tolerated receiving requests to resume body streaming in the finished state. Result Fewer crashes Fixes #576
1 parent b1e4f19 commit f50bf98

File tree

3 files changed

+45
-1
lines changed

3 files changed

+45
-1
lines changed

Sources/AsyncHTTPClient/RequestBag+StateMachine.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ extension RequestBag.StateMachine {
127127
return .none
128128

129129
case .finished:
130-
preconditionFailure("Invalid state: \(self.state)")
130+
// If this task has been cancelled we may be in an error state. As a matter of
131+
// defensive programming, we also tolerate receiving this notification if we've ended cleanly:
132+
// while it shouldn't happen, nothing will go wrong if we just ignore it.
133+
// All paths through this state machine should cancel our request body stream to get here anyway.
134+
return .none
131135

132136
case .modifying:
133137
preconditionFailure("Invalid state: \(self.state)")

Tests/AsyncHTTPClientTests/RequestBagTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ extension RequestBagTests {
3131
("testCancelFailsTaskAfterRequestIsSent", testCancelFailsTaskAfterRequestIsSent),
3232
("testCancelFailsTaskWhenTaskIsQueued", testCancelFailsTaskWhenTaskIsQueued),
3333
("testFailsTaskWhenTaskIsWaitingForMoreFromServer", testFailsTaskWhenTaskIsWaitingForMoreFromServer),
34+
("testChannelBecomingWritableDoesntCrashCancelledTask", testChannelBecomingWritableDoesntCrashCancelledTask),
3435
("testHTTPUploadIsCancelledEvenThoughRequestSucceeds", testHTTPUploadIsCancelledEvenThoughRequestSucceeds),
3536
("testRaceBetweenConnectionCloseAndDemandMoreData", testRaceBetweenConnectionCloseAndDemandMoreData),
3637
]

Tests/AsyncHTTPClientTests/RequestBagTests.swift

+39
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,45 @@ final class RequestBagTests: XCTestCase {
336336
}
337337
}
338338

339+
func testChannelBecomingWritableDoesntCrashCancelledTask() {
340+
let embeddedEventLoop = EmbeddedEventLoop()
341+
defer { XCTAssertNoThrow(try embeddedEventLoop.syncShutdownGracefully()) }
342+
let logger = Logger(label: "test")
343+
344+
var maybeRequest: HTTPClient.Request?
345+
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(
346+
url: "https://swift.org",
347+
body: .bytes([1, 2, 3, 4, 5])
348+
))
349+
guard let request = maybeRequest else { return XCTFail("Expected to have a request") }
350+
351+
let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop)
352+
var maybeRequestBag: RequestBag<UploadCountingDelegate>?
353+
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
354+
request: request,
355+
eventLoopPreference: .delegate(on: embeddedEventLoop),
356+
task: .init(eventLoop: embeddedEventLoop, logger: logger),
357+
redirectHandler: nil,
358+
connectionDeadline: .now() + .seconds(30),
359+
requestOptions: .forTests(),
360+
delegate: delegate
361+
))
362+
guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
363+
364+
let executor = MockRequestExecutor(eventLoop: embeddedEventLoop)
365+
executor.runRequest(bag)
366+
367+
// This simulates a race between the user cancelling the task (which invokes `RequestBag.cancel`) and the
368+
// call to `resumeRequestBodyStream` (which comes from the `Channel` event loop and so may have to hop.
369+
bag.cancel()
370+
bag.resumeRequestBodyStream()
371+
372+
XCTAssertEqual(executor.isCancelled, true)
373+
XCTAssertThrowsError(try bag.task.futureResult.wait()) {
374+
XCTAssertEqual($0 as? HTTPClientError, .cancelled)
375+
}
376+
}
377+
339378
func testHTTPUploadIsCancelledEvenThoughRequestSucceeds() {
340379
let embeddedEventLoop = EmbeddedEventLoop()
341380
defer { XCTAssertNoThrow(try embeddedEventLoop.syncShutdownGracefully()) }

0 commit comments

Comments
 (0)