diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 68fe21bcc..d39c600b5 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -668,7 +668,16 @@ public protocol HTTPClientResponseDelegate: AnyObject { /// - task: Current request context. func didSendRequest(task: HTTPClient.Task) - /// Called when response head is received. Will be called once. + /// Called each time a response head is received (including redirects), and always called before ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd``. + /// You can use this method to keep an entire history of the request/response chain. + /// + /// - parameters: + /// - task: Current request context. + /// - request: The request that was sent. + /// - head: Received response head. + func didVisitURL(task: HTTPClient.Task, _ request: HTTPClient.Request, _ head: HTTPResponseHead) + + /// Called when the final response head is received (after redirects). /// You must return an `EventLoopFuture` that you complete when you have finished processing the body part. /// You can create an already succeeded future by calling `task.eventLoop.makeSucceededFuture(())`. /// @@ -734,6 +743,11 @@ extension HTTPClientResponseDelegate { /// By default, this does nothing. public func didSendRequest(task: HTTPClient.Task) {} + /// Default implementation of ``HTTPClientResponseDelegate/didVisitURL(task:_:_:)-2el9y``. + /// + /// By default, this does nothing. + public func didVisitURL(task: HTTPClient.Task, _: HTTPClient.Request, _: HTTPResponseHead) {} + /// Default implementation of ``HTTPClientResponseDelegate/didReceiveHead(task:_:)-9r4xd``. /// /// By default, this does nothing. diff --git a/Sources/AsyncHTTPClient/RequestBag.swift b/Sources/AsyncHTTPClient/RequestBag.swift index bc6325602..9255e7c21 100644 --- a/Sources/AsyncHTTPClient/RequestBag.swift +++ b/Sources/AsyncHTTPClient/RequestBag.swift @@ -228,6 +228,8 @@ final class RequestBag { private func receiveResponseHead0(_ head: HTTPResponseHead) { self.task.eventLoop.assertInEventLoop() + self.delegate.didVisitURL(task: self.task, self.request, head) + // runs most likely on channel eventLoop switch self.state.receiveResponseHead(head) { case .none: diff --git a/Tests/AsyncHTTPClientTests/RequestBagTests.swift b/Tests/AsyncHTTPClientTests/RequestBagTests.swift index 9aa595224..fa92b84bd 100644 --- a/Tests/AsyncHTTPClientTests/RequestBagTests.swift +++ b/Tests/AsyncHTTPClientTests/RequestBagTests.swift @@ -127,6 +127,7 @@ final class RequestBagTests: XCTestCase { XCTAssertNoThrow(try executor.receiveEndOfStream()) XCTAssertEqual(receivedBytes, bytesToSent, "We have sent all request bytes...") + XCTAssertTrue(delegate.history.isEmpty) XCTAssertNil(delegate.receivedHead, "Expected not to have a response head, before `receiveResponseHead`") let responseHead = HTTPResponseHead( version: .http1_1, @@ -140,6 +141,10 @@ final class RequestBagTests: XCTestCase { XCTAssertEqual(responseHead, delegate.receivedHead) XCTAssertNoThrow(try XCTUnwrap(delegate.backpressurePromise).succeed(())) XCTAssertTrue(executor.signalledDemandForResponseBody) + + XCTAssertEqual(delegate.history.map(\.request.url), [request.url]) + XCTAssertEqual(delegate.history.map(\.response), [responseHead]) + executor.resetResponseStreamDemandSignal() // we will receive 20 chunks with each 10 byteBuffers and 32 bytes @@ -747,13 +752,15 @@ final class RequestBagTests: XCTestCase { let executor = MockRequestExecutor(eventLoop: embeddedEventLoop) executor.runRequest(bag) XCTAssertFalse(executor.signalledDemandForResponseBody) - bag.receiveResponseHead( - .init( - version: .http1_1, - status: .permanentRedirect, - headers: ["content-length": "\(3 * 1024)", "location": "https://swift.org/sswg"] - ) + XCTAssertTrue(delegate.history.isEmpty) + let responseHead = HTTPResponseHead( + version: .http1_1, + status: .permanentRedirect, + headers: ["content-length": "\(3 * 1024)", "location": "https://swift.org/sswg"] ) + bag.receiveResponseHead(responseHead) + XCTAssertEqual(delegate.history.map(\.request.url), [request.url]) + XCTAssertEqual(delegate.history.map(\.response), [responseHead]) XCTAssertNil(delegate.backpressurePromise) XCTAssertTrue(executor.signalledDemandForResponseBody) executor.resetResponseStreamDemandSignal() @@ -833,13 +840,15 @@ final class RequestBagTests: XCTestCase { let executor = MockRequestExecutor(eventLoop: embeddedEventLoop) executor.runRequest(bag) XCTAssertFalse(executor.signalledDemandForResponseBody) - bag.receiveResponseHead( - .init( - version: .http1_1, - status: .permanentRedirect, - headers: ["content-length": "\(4 * 1024)", "location": "https://swift.org/sswg"] - ) + XCTAssertTrue(delegate.history.isEmpty) + let responseHead = HTTPResponseHead( + version: .http1_1, + status: .permanentRedirect, + headers: ["content-length": "\(4 * 1024)", "location": "https://swift.org/sswg"] ) + bag.receiveResponseHead(responseHead) + XCTAssertEqual(delegate.history.map(\.request.url), [request.url]) + XCTAssertEqual(delegate.history.map(\.response), [responseHead]) XCTAssertNil(delegate.backpressurePromise) XCTAssertFalse(executor.signalledDemandForResponseBody) XCTAssertTrue(executor.isCancelled) @@ -893,13 +902,15 @@ final class RequestBagTests: XCTestCase { let executor = MockRequestExecutor(eventLoop: embeddedEventLoop) executor.runRequest(bag) XCTAssertFalse(executor.signalledDemandForResponseBody) - bag.receiveResponseHead( - .init( - version: .http1_1, - status: .permanentRedirect, - headers: ["content-length": "\(3 * 1024)", "location": "https://swift.org/sswg"] - ) + XCTAssertTrue(delegate.history.isEmpty) + let responseHead = HTTPResponseHead( + version: .http1_1, + status: .permanentRedirect, + headers: ["content-length": "\(3 * 1024)", "location": "https://swift.org/sswg"] ) + bag.receiveResponseHead(responseHead) + XCTAssertEqual(delegate.history.map(\.request.url), [request.url]) + XCTAssertEqual(delegate.history.map(\.response), [responseHead]) XCTAssertNil(delegate.backpressurePromise) XCTAssertTrue(executor.signalledDemandForResponseBody) executor.resetResponseStreamDemandSignal() @@ -1001,6 +1012,7 @@ class UploadCountingDelegate: HTTPClientResponseDelegate { private(set) var hitDidReceiveBodyPart = 0 private(set) var hitDidReceiveError = 0 + private(set) var history: [(request: HTTPClient.Request, response: HTTPResponseHead)] = [] private(set) var receivedHead: HTTPResponseHead? private(set) var lastBodyPart: ByteBuffer? private(set) var backpressurePromise: EventLoopPromise? @@ -1022,6 +1034,10 @@ class UploadCountingDelegate: HTTPClientResponseDelegate { self.hitDidSendRequest += 1 } + func didVisitURL(task: HTTPClient.Task, _ request: HTTPClient.Request, _ head: HTTPResponseHead) { + self.history.append((request, head)) + } + func didReceiveHead(task: HTTPClient.Task, _ head: HTTPResponseHead) -> EventLoopFuture { self.receivedHead = head return self.createBackpressurePromise()