Skip to content

Commit b595e3b

Browse files
committed
New test
1 parent d381066 commit b595e3b

File tree

5 files changed

+88
-6
lines changed

5 files changed

+88
-6
lines changed

Diff for: Sources/AsyncHTTPClient/ConnectionPool/HTTPExecutingRequest.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,19 @@ protocol HTTPExecutingRequest: AnyObject {
226226
/// to ask for more data.
227227
func receiveResponseHead(_ head: HTTPResponseHead)
228228

229-
/// Receive a response body stream part.
229+
/// Receive response body stream parts.
230230
///
231231
/// Please note that `receiveResponseHead` and `receiveResponseBodyPart` may
232232
/// be called in quick succession. It is the task's job to buffer those events for the user. Once all
233233
/// buffered data has been consumed the task must call `executor.demandResponseBodyStream`
234234
/// to ask for more data.
235235
func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>)
236236

237+
/// Succeeds the executing request. The executor will not call any further methods on the request after this method.
238+
///
239+
/// - Parameter buffer: The remaining response body parts, that were received before the request end
237240
func succeedRequest(_ buffer: CircularBuffer<ByteBuffer>?)
238241

242+
/// Fails the executing request, with an error.
239243
func fail(_ error: Error)
240244
}

Diff for: Sources/AsyncHTTPClient/HTTPClient.swift

+5
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
923923
case httpProxyHandshakeTimeout
924924
case tlsHandshakeTimeout
925925
case serverOfferedUnsupportedApplicationProtocol(String)
926+
case requestStreamCancelled
926927
}
927928

928929
private var code: Code
@@ -991,4 +992,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
991992
public static func serverOfferedUnsupportedApplicationProtocol(_ proto: String) -> HTTPClientError {
992993
return HTTPClientError(code: .serverOfferedUnsupportedApplicationProtocol(proto))
993994
}
995+
996+
/// The remote server responded with a status code between >= 300, before the full request was send. The request stream
997+
/// was therefore cancelled
998+
public static let requestStreamCancelled = HTTPClientError(code: .requestStreamCancelled)
994999
}

Diff for: Sources/AsyncHTTPClient/RequestBag+StateMachine.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ extension RequestBag.StateMachine {
203203
case .finished(error: .some(let error)):
204204
return .failFuture(error)
205205
case .finished(error: .none):
206-
preconditionFailure("A write was made, after the request has completed")
206+
return .failFuture(HTTPClientError.requestStreamCancelled)
207207
case .modifying:
208208
preconditionFailure("Invalid state")
209209
}
@@ -338,7 +338,10 @@ extension RequestBag.StateMachine {
338338
preconditionFailure("If we have received an error or eof before, why did we get another body part? Next: \(next)")
339339
}
340340

341-
if buffer.isEmpty, let newChunks = newChunks {
341+
if buffer.isEmpty, newChunks == nil || newChunks!.isEmpty {
342+
self.state = .finished(error: nil)
343+
return .succeedRequest
344+
} else if buffer.isEmpty, let newChunks = newChunks {
342345
buffer = newChunks
343346
} else if let newChunks = newChunks {
344347
buffer.append(contentsOf: newChunks)

Diff for: Tests/AsyncHTTPClientTests/RequestBagTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ extension RequestBagTests {
3030
("testCancelFailsTaskBeforeRequestIsSent", testCancelFailsTaskBeforeRequestIsSent),
3131
("testCancelFailsTaskAfterRequestIsSent", testCancelFailsTaskAfterRequestIsSent),
3232
("testCancelFailsTaskWhenTaskIsQueued", testCancelFailsTaskWhenTaskIsQueued),
33+
("testHTTPUploadIsCancelledEvenThoughRequestSucceeds", testHTTPUploadIsCancelledEvenThoughRequestSucceeds),
3334
]
3435
}
3536
}

Diff for: Tests/AsyncHTTPClientTests/RequestBagTests.swift

+72-3
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ final class RequestBagTests: XCTestCase {
6666
)
6767
XCTAssert(bag.task.eventLoop === embeddedEventLoop)
6868

69-
let executor = MockRequestExecutor()
69+
let executor = MockRequestExecutor(pauseRequestBodyPartStreamAfterASingleWrite: true)
7070

7171
bag.willExecuteRequest(executor)
7272

@@ -294,6 +294,71 @@ final class RequestBagTests: XCTestCase {
294294
XCTAssertEqual($0 as? HTTPClientError, .cancelled)
295295
}
296296
}
297+
298+
func testHTTPUploadIsCancelledEvenThoughRequestSucceeds() {
299+
let embeddedEventLoop = EmbeddedEventLoop()
300+
defer { XCTAssertNoThrow(try embeddedEventLoop.syncShutdownGracefully()) }
301+
let logger = Logger(label: "test")
302+
303+
var maybeRequest: HTTPClient.Request?
304+
let writeSecondPartPromise = embeddedEventLoop.makePromise(of: Void.self)
305+
306+
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(
307+
url: "https://swift.org",
308+
method: .POST,
309+
headers: ["content-length": "12"],
310+
body: .stream(length: 12) { writer -> EventLoopFuture<Void> in
311+
var firstWriteSuccess = false
312+
return writer.write(.byteBuffer(.init(bytes: 0...3))).flatMap { _ in
313+
firstWriteSuccess = true
314+
315+
return writeSecondPartPromise.futureResult
316+
}.flatMap {
317+
return writer.write(.byteBuffer(.init(bytes: 4...7)))
318+
}.always { result in
319+
XCTAssertTrue(firstWriteSuccess)
320+
321+
guard case .failure(let error) = result else {
322+
return XCTFail("Expected the second write to fail")
323+
}
324+
XCTAssertEqual(error as? HTTPClientError, .requestStreamCancelled)
325+
}
326+
}
327+
))
328+
guard let request = maybeRequest else { return XCTFail("Expected to have a request") }
329+
330+
let delegate = UploadCountingDelegate(eventLoop: embeddedEventLoop)
331+
let bag = RequestBag(
332+
request: request,
333+
eventLoopPreference: .delegate(on: embeddedEventLoop),
334+
task: .init(eventLoop: embeddedEventLoop, logger: logger),
335+
redirectHandler: nil,
336+
connectionDeadline: .now() + .seconds(30),
337+
idleReadTimeout: nil,
338+
delegate: delegate
339+
)
340+
341+
let executor = MockRequestExecutor()
342+
bag.willExecuteRequest(executor)
343+
344+
XCTAssertEqual(delegate.hitDidSendRequestHead, 0)
345+
XCTAssertEqual(delegate.hitDidSendRequest, 0)
346+
bag.requestHeadSent()
347+
XCTAssertEqual(delegate.hitDidSendRequestHead, 1)
348+
XCTAssertEqual(delegate.hitDidSendRequest, 0)
349+
350+
bag.resumeRequestBodyStream()
351+
XCTAssertEqual(executor.nextBodyPart(), .body(.byteBuffer(.init(bytes: 0...3))))
352+
// receive a 301 response immediately.
353+
bag.receiveResponseHead(.init(version: .http1_1, status: .movedPermanently))
354+
bag.succeedRequest(.init())
355+
356+
// if we now write our second part of the response this should fail the backpressure promise
357+
writeSecondPartPromise.succeed(())
358+
359+
XCTAssertEqual(delegate.receivedHead?.status, .movedPermanently)
360+
XCTAssertNoThrow(try bag.task.futureResult.wait())
361+
}
297362
}
298363

299364
class MockRequestExecutor: HTTPRequestExecutor {
@@ -302,11 +367,15 @@ class MockRequestExecutor: HTTPRequestExecutor {
302367
case endOfStream
303368
}
304369

370+
let pauseRequestBodyPartStreamAfterASingleWrite: Bool
371+
305372
private(set) var requestBodyParts = CircularBuffer<RequestParts>()
306373
private(set) var isCancelled: Bool = false
307374
private(set) var signalledDemandForResponseBody: Bool = false
308375

309-
init() {}
376+
init(pauseRequestBodyPartStreamAfterASingleWrite: Bool = false) {
377+
self.pauseRequestBodyPartStreamAfterASingleWrite = pauseRequestBodyPartStreamAfterASingleWrite
378+
}
310379

311380
func nextBodyPart() -> RequestParts? {
312381
guard !self.requestBodyParts.isEmpty else { return nil }
@@ -321,7 +390,7 @@ class MockRequestExecutor: HTTPRequestExecutor {
321390
// data is already scheduled. If we call pause here, once, after the second call new subsequent
322391
// calls should not be scheduled.
323392
func writeRequestBodyPart(_ part: IOData, request: HTTPExecutingRequest) {
324-
if self.requestBodyParts.isEmpty {
393+
if self.requestBodyParts.isEmpty, self.pauseRequestBodyPartStreamAfterASingleWrite {
325394
request.pauseRequestBodyStream()
326395
}
327396
self.requestBodyParts.append(.body(part))

0 commit comments

Comments
 (0)