Skip to content

Commit 316cbf9

Browse files
authored
[RequestBag] Fix consumption error in state machine (#441)
1 parent 2a47a1d commit 316cbf9

File tree

3 files changed

+64
-7
lines changed

3 files changed

+64
-7
lines changed

Sources/AsyncHTTPClient/RequestBag+StateMachine.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -342,12 +342,9 @@ extension RequestBag.StateMachine {
342342
preconditionFailure("If we have received an error or eof before, why did we get another body part? Next: \(next)")
343343
}
344344

345-
if buffer.isEmpty, newChunks == nil || newChunks!.isEmpty {
346-
self.state = .finished(error: nil)
347-
return .succeedRequest
348-
} else if buffer.isEmpty, let newChunks = newChunks {
345+
if buffer.isEmpty, let newChunks = newChunks, !newChunks.isEmpty {
349346
buffer = newChunks
350-
} else if let newChunks = newChunks {
347+
} else if let newChunks = newChunks, !newChunks.isEmpty {
351348
buffer.append(contentsOf: newChunks)
352349
}
353350

@@ -433,9 +430,11 @@ extension RequestBag.StateMachine {
433430
private mutating func consumeMoreBodyData() -> ConsumeAction {
434431
switch self.state {
435432
case .initialized, .queued:
436-
preconditionFailure("Invalid state")
433+
preconditionFailure("Invalid state: \(self.state)")
434+
437435
case .executing(_, _, .initialized):
438436
preconditionFailure("Invalid state: Must have received response head, before this method is called for the first time")
437+
439438
case .executing(let executor, let requestState, .buffering(var buffer, next: .askExecutorForMore)):
440439
self.state = .modifying
441440

@@ -473,7 +472,7 @@ extension RequestBag.StateMachine {
473472
return .doNothing
474473

475474
case .finished(error: .none):
476-
preconditionFailure("Invalid state... If no error occured, this must not be called, after the request was finished")
475+
preconditionFailure("Invalid state... If no error occurred, this must not be called, after the request was finished")
477476

478477
case .modifying:
479478
preconditionFailure()

Tests/AsyncHTTPClientTests/RequestBagTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ extension RequestBagTests {
3232
("testCancelFailsTaskWhenTaskIsQueued", testCancelFailsTaskWhenTaskIsQueued),
3333
("testFailsTaskWhenTaskIsWaitingForMoreFromServer", testFailsTaskWhenTaskIsWaitingForMoreFromServer),
3434
("testHTTPUploadIsCancelledEvenThoughRequestSucceeds", testHTTPUploadIsCancelledEvenThoughRequestSucceeds),
35+
("testRaceBetweenConnectionCloseAndDemandMoreData", testRaceBetweenConnectionCloseAndDemandMoreData),
3536
]
3637
}
3738
}

Tests/AsyncHTTPClientTests/RequestBagTests.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ final class RequestBagTests: XCTestCase {
399399
XCTAssertEqual(executor.nextBodyPart(), .body(.byteBuffer(.init(bytes: 0...3))))
400400
// receive a 301 response immediately.
401401
bag.receiveResponseHead(.init(version: .http1_1, status: .movedPermanently))
402+
XCTAssertNoThrow(try XCTUnwrap(delegate.backpressurePromise).succeed(()))
402403
bag.succeedRequest(.init())
403404

404405
// if we now write our second part of the response this should fail the backpressure promise
@@ -407,6 +408,62 @@ final class RequestBagTests: XCTestCase {
407408
XCTAssertEqual(delegate.receivedHead?.status, .movedPermanently)
408409
XCTAssertNoThrow(try bag.task.futureResult.wait())
409410
}
411+
412+
func testRaceBetweenConnectionCloseAndDemandMoreData() {
413+
let embeddedEventLoop = EmbeddedEventLoop()
414+
defer { XCTAssertNoThrow(try embeddedEventLoop.syncShutdownGracefully()) }
415+
let logger = Logger(label: "test")
416+
417+
var maybeRequest: HTTPClient.Request?
418+
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://swift.org"))
419+
guard let request = maybeRequest else { return XCTFail("Expected to have a request") }
420+
421+
let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop)
422+
var maybeRequestBag: RequestBag<UploadCountingDelegate>?
423+
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
424+
request: request,
425+
eventLoopPreference: .delegate(on: embeddedEventLoop),
426+
task: .init(eventLoop: embeddedEventLoop, logger: logger),
427+
redirectHandler: nil,
428+
connectionDeadline: .now() + .seconds(30),
429+
requestOptions: .forTests(),
430+
delegate: delegate
431+
))
432+
guard let bag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
433+
434+
let executor = MockRequestExecutor()
435+
bag.willExecuteRequest(executor)
436+
bag.requestHeadSent()
437+
bag.receiveResponseHead(.init(version: .http1_1, status: .ok))
438+
XCTAssertFalse(executor.signalledDemandForResponseBody)
439+
XCTAssertNoThrow(try XCTUnwrap(delegate.backpressurePromise).succeed(()))
440+
XCTAssertTrue(executor.signalledDemandForResponseBody)
441+
executor.resetDemandSignal()
442+
443+
// "foo" is forwarded for consumption. We expect the RequestBag to consume "foo" with the
444+
// delegate and call demandMoreBody afterwards.
445+
XCTAssertEqual(delegate.hitDidReceiveBodyPart, 0)
446+
bag.receiveResponseBodyParts([ByteBuffer(string: "foo")])
447+
XCTAssertFalse(executor.signalledDemandForResponseBody)
448+
XCTAssertEqual(delegate.hitDidReceiveBodyPart, 1)
449+
XCTAssertNoThrow(try XCTUnwrap(delegate.backpressurePromise).succeed(()))
450+
XCTAssertTrue(executor.signalledDemandForResponseBody)
451+
executor.resetDemandSignal()
452+
453+
bag.receiveResponseBodyParts([ByteBuffer(string: "bar")])
454+
XCTAssertEqual(delegate.hitDidReceiveBodyPart, 2)
455+
456+
// the remote closes the connection, which leads to more data and a succeed of the request
457+
bag.succeedRequest([ByteBuffer(string: "baz")])
458+
XCTAssertEqual(delegate.hitDidReceiveBodyPart, 2)
459+
460+
XCTAssertNoThrow(try XCTUnwrap(delegate.backpressurePromise).succeed(()))
461+
XCTAssertEqual(delegate.hitDidReceiveBodyPart, 3)
462+
463+
XCTAssertEqual(delegate.hitDidReceiveResponse, 0)
464+
XCTAssertNoThrow(try XCTUnwrap(delegate.backpressurePromise).succeed(()))
465+
XCTAssertEqual(delegate.hitDidReceiveResponse, 1)
466+
}
410467
}
411468

412469
class MockRequestExecutor: HTTPRequestExecutor {

0 commit comments

Comments
 (0)