ソースを参照

Update cURL support for async request creation. (#2863)

* Connect events to cURL description.

* Clean up and add more tests.

* Add DataEncoding and KeyEncoding. (#2858)

* Appending response serializer now resumes request if finished to handle races (#2862)
Jon Shier 6 年 前
コミット
55846392ad

+ 25 - 9
Source/EventMonitor.swift

@@ -96,8 +96,9 @@ public protocol EventMonitor {
 
     // MARK: - Request Events
 
-    /// Event called when a `URLRequest` is first created for a `Request`.
-    func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest)
+    /// Event called when a `URLRequest` is first created for a `Request`. If a `RequestAdapter` is active, the
+    /// `URLRequest` will be adapted before being issued.
+    func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest)
 
     /// Event called when the attempt to create a `URLRequest` from a `Request`'s original `URLRequestConvertible` value fails.
     func request(_ request: Request, didFailToCreateURLRequestWithError error: Error)
@@ -108,6 +109,9 @@ public protocol EventMonitor {
     /// Event called when a `RequestAdapter` fails to adapt the `Request`'s initial `URLRequest`.
     func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: Error)
 
+    /// Event called when a final `URLRequest` is created for a `Request`.
+    func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest)
+
     /// Event called when a `URLSessionTask` subclass instance is created for a `Request`.
     func request(_ request: Request, didCreateTask task: URLSessionTask)
 
@@ -236,7 +240,7 @@ extension EventMonitor {
     public func urlSession(_ session: URLSession,
                            downloadTask: URLSessionDownloadTask,
                            didFinishDownloadingTo location: URL) { }
-    public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { }
+    public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) { }
     public func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) { }
     public func request(_ request: Request,
                         didAdaptInitialRequest initialRequest: URLRequest,
@@ -244,6 +248,7 @@ extension EventMonitor {
     public func request(_ request: Request,
                         didFailToAdaptURLRequest initialRequest: URLRequest,
                         withError error: Error) { }
+    public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { }
     public func request(_ request: Request, didCreateTask task: URLSessionTask) { }
     public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { }
     public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: Error) { }
@@ -392,8 +397,8 @@ public final class CompositeEventMonitor: EventMonitor {
         performEvent { $0.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) }
     }
 
-    public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {
-        performEvent { $0.request(request, didCreateURLRequest: urlRequest) }
+    public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {
+        performEvent { $0.request(request, didCreateInitialURLRequest: urlRequest) }
     }
 
     public func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) {
@@ -408,6 +413,10 @@ public final class CompositeEventMonitor: EventMonitor {
         performEvent { $0.request(request, didFailToAdaptURLRequest: initialRequest, withError: error) }
     }
 
+    public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {
+        performEvent { $0.request(request, didCreateURLRequest: urlRequest) }
+    }
+
     public func request(_ request: Request, didCreateTask task: URLSessionTask) {
         performEvent { $0.request(request, didCreateTask: task) }
     }
@@ -562,8 +571,8 @@ open class ClosureEventMonitor: EventMonitor {
 
     // MARK: - Request Events
 
-    /// Closure called on the `request(_:didCreateURLRequest:)` event.
-    open var requestDidCreateURLRequest: ((Request, URLRequest) -> Void)?
+    /// Closure called on the `request(_:didCreateInitialURLRequest:)` event.
+    open var requestDidCreateInitialURLRequest: ((Request, URLRequest) -> Void)?
 
     /// Closure called on the `request(_:didFailToCreateURLRequestWithError:)` event.
     open var requestDidFailToCreateURLRequestWithError: ((Request, Error) -> Void)?
@@ -574,6 +583,9 @@ open class ClosureEventMonitor: EventMonitor {
     /// Closure called on the `request(_:didFailToAdaptURLRequest:withError:)` event.
     open var requestDidFailToAdaptURLRequestWithError: ((Request, URLRequest, Error) -> Void)?
 
+    /// Closure called on the `request(_:didCreateURLRequest:)` event.
+    open var requestDidCreateURLRequest: ((Request, URLRequest) -> Void)?
+
     /// Closure called on the `request(_:didCreateTask:)` event.
     open var requestDidCreateTask: ((Request, URLSessionTask) -> Void)?
 
@@ -714,8 +726,8 @@ open class ClosureEventMonitor: EventMonitor {
 
     // MARK: Request Events
 
-    open func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {
-        requestDidCreateURLRequest?(request, urlRequest)
+    open func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {
+        requestDidCreateInitialURLRequest?(request, urlRequest)
     }
 
     open func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) {
@@ -730,6 +742,10 @@ open class ClosureEventMonitor: EventMonitor {
         requestDidFailToAdaptURLRequestWithError?(request, initialRequest, error)
     }
 
+    open func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {
+        requestDidCreateURLRequest?(request, urlRequest)
+    }
+
     open func request(_ request: Request, didCreateTask task: URLSessionTask) {
         requestDidCreateTask?(request, task)
     }

+ 58 - 15
Source/Request.swift

@@ -92,6 +92,8 @@ public class Request {
         var redirectHandler: RedirectHandler?
         /// `CachedResponseHandler` provided to handle response caching.
         var cachedResponseHandler: CachedResponseHandler?
+        /// Closure called when the `Request` is able to create a cURL description of itself.
+        var cURLHandler: ((String) -> Void)?
         /// Response serialization closures that handle response parsing.
         var responseSerializers: [() -> Void] = []
         /// Response serialization completion closures executed once all response serializers are complete.
@@ -263,13 +265,14 @@ public class Request {
     // MARK: - Internal Event API
     // All API must be called from underlyingQueue.
 
-    /// Called when a `URLRequest` has been created on behalf of the instance.
+    /// Called when a initial `URLRequest` has been created on behalf of the instance. If a `RequestAdapter` is active,
+    /// the `URLRequest` will be adapted before being issued.
     ///
-    /// - Parameter request: `URLRequest` created.
-    func didCreateURLRequest(_ request: URLRequest) {
+    /// - Parameter request: The `URLRequest` created.
+    func didCreateInitialURLRequest(_ request: URLRequest) {
         protectedMutableState.write { $0.requests.append(request) }
 
-        eventMonitor?.request(self, didCreateURLRequest: request)
+        eventMonitor?.request(self, didCreateInitialURLRequest: request)
     }
 
     /// Called when initial `URLRequest` creation has failed, typically through a `URLRequestConvertible`.
@@ -282,6 +285,8 @@ public class Request {
 
         eventMonitor?.request(self, didFailToCreateURLRequestWithError: error)
 
+        callCURLHandlerIfNecessary()
+
         retryOrFinish(error: error)
     }
 
@@ -308,9 +313,30 @@ public class Request {
 
         eventMonitor?.request(self, didFailToAdaptURLRequest: request, withError: error)
 
+        callCURLHandlerIfNecessary()
+
         retryOrFinish(error: error)
     }
 
+    /// Final `URLRequest` has been created for the instance.
+    ///
+    /// - Parameter request: The `URLRequest` created.
+    func didCreateURLRequest(_ request: URLRequest) {
+        eventMonitor?.request(self, didCreateURLRequest: request)
+
+        callCURLHandlerIfNecessary()
+    }
+
+    /// Asynchronously calls any stored `cURLHandler` and then removes it from `mutableState`.
+    private func callCURLHandlerIfNecessary() {
+        protectedMutableState.write { mutableState in
+            guard let cURLHandler = mutableState.cURLHandler else { return }
+
+            self.underlyingQueue.async { cURLHandler(self.cURLDescription()) }
+            mutableState.cURLHandler = nil
+        }
+    }
+
     /// Called when a `URLSessionTask` is created on behalf of the instance.
     ///
     /// - Parameter task: The `URLSessionTask` created.
@@ -693,7 +719,7 @@ public class Request {
 
     /// Sets the redirect handler for the instance which will be used if a redirect response is encountered.
     ///
-    /// - Note: Overrides any `RedirectHandler` set on the `Session` which produced this `Request`.
+    /// - Note: Attempting to set the redirect handler more than once is a logic error and will crash.
     ///
     /// - Parameter handler: The `RedirectHandler`.
     ///
@@ -701,7 +727,7 @@ public class Request {
     @discardableResult
     public func redirect(using handler: RedirectHandler) -> Self {
         protectedMutableState.write { mutableState in
-            precondition(mutableState.redirectHandler == nil, "Redirect handler has already been set")
+            precondition(mutableState.redirectHandler == nil, "Redirect handler has already been set.")
             mutableState.redirectHandler = handler
         }
 
@@ -712,19 +738,41 @@ public class Request {
 
     /// Sets the cached response handler for the `Request` which will be used when attempting to cache a response.
     ///
+    /// - Note: Attempting to set the cache handler more than once is a logic error and will crash.
+    ///
     /// - Parameter handler: The `CachedResponseHandler`.
     ///
-    /// - Returns:           The `Request`.
+    /// - Returns:           The instance.
     @discardableResult
     public func cacheResponse(using handler: CachedResponseHandler) -> Self {
         protectedMutableState.write { mutableState in
-            precondition(mutableState.cachedResponseHandler == nil, "Cached response handler has already been set")
+            precondition(mutableState.cachedResponseHandler == nil, "Cached response handler has already been set.")
             mutableState.cachedResponseHandler = handler
         }
 
         return self
     }
 
+    /// Sets a handler to be called when the cURL description of the request is available.
+    ///
+    /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called.
+    ///
+    /// - Parameter handler: Closure to be called when the cURL description is available.
+    ///
+    /// - Returns:           The instance.
+    @discardableResult
+    public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self {
+        protectedMutableState.write { mutableState in
+            if mutableState.requests.last != nil {
+                underlyingQueue.async { handler(self.cURLDescription()) }
+            } else {
+                mutableState.cURLHandler = handler
+            }
+        }
+
+        return self
+    }
+
     // MARK: Cleanup
 
     /// Final cleanup step executed when the instance finishes response serialization.
@@ -761,16 +809,11 @@ extension Request: CustomStringConvertible {
     }
 }
 
-extension Request: CustomDebugStringConvertible {
-    /// A textual representation of this instance in the form of a cURL command.
-    public var debugDescription: String {
-        return cURLRepresentation()
-    }
-
+extension Request {
     /// cURL representation of the instance.
     ///
     /// - Returns: The cURL equivalent of the instance.
-    func cURLRepresentation() -> String {
+    public func cURLDescription() -> String {
         guard
             let request = lastRequest,
             let url = request.url,

+ 3 - 1
Source/Session.swift

@@ -798,7 +798,7 @@ open class Session {
     func performSetupOperations(for request: Request, convertible: URLRequestConvertible) {
         do {
             let initialRequest = try convertible.asURLRequest()
-            rootQueue.async { request.didCreateURLRequest(initialRequest) }
+            rootQueue.async { request.didCreateInitialURLRequest(initialRequest) }
 
             guard !request.isCancelled else { return }
 
@@ -827,6 +827,8 @@ open class Session {
     // MARK: - Task Handling
 
     func didCreateURLRequest(_ urlRequest: URLRequest, for request: Request) {
+        request.didCreateURLRequest(urlRequest)
+
         guard !request.isCancelled else { return }
 
         let task = request.task(for: urlRequest, using: session)

+ 4 - 0
Tests/DownloadTests.swift

@@ -388,6 +388,7 @@ final class DownloadRequestEventsTestCase: BaseTestCase {
         let session = Session(eventMonitors: [eventMonitor])
 
         let taskDidFinishCollecting = expectation(description: "taskDidFinishCollecting should fire")
+        let didCreateInitialURLRequest = expectation(description: "didCreateInitialURLRequest should fire")
         let didCreateURLRequest = expectation(description: "didCreateURLRequest should fire")
         let didCreateTask = expectation(description: "didCreateTask should fire")
         let didGatherMetrics = expectation(description: "didGatherMetrics should fire")
@@ -405,6 +406,7 @@ final class DownloadRequestEventsTestCase: BaseTestCase {
         var wroteData = false
 
         eventMonitor.taskDidFinishCollectingMetrics = { (_, _, _) in taskDidFinishCollecting.fulfill() }
+        eventMonitor.requestDidCreateInitialURLRequest = { (_, _) in didCreateInitialURLRequest.fulfill() }
         eventMonitor.requestDidCreateURLRequest = { (_, _) in didCreateURLRequest.fulfill() }
         eventMonitor.requestDidCreateTask = { (_, _) in didCreateTask.fulfill() }
         eventMonitor.requestDidGatherMetrics = { (_, _) in didGatherMetrics.fulfill() }
@@ -440,6 +442,7 @@ final class DownloadRequestEventsTestCase: BaseTestCase {
         let session = Session(startRequestsImmediately: false, eventMonitors: [eventMonitor])
 
         let taskDidFinishCollecting = expectation(description: "taskDidFinishCollecting should fire")
+        let didCreateInitialURLRequest = expectation(description: "didCreateInitialURLRequest should fire")
         let didCreateURLRequest = expectation(description: "didCreateURLRequest should fire")
         let didCreateTask = expectation(description: "didCreateTask should fire")
         let didGatherMetrics = expectation(description: "didGatherMetrics should fire")
@@ -453,6 +456,7 @@ final class DownloadRequestEventsTestCase: BaseTestCase {
         let responseHandler = expectation(description: "responseHandler should fire")
 
         eventMonitor.taskDidFinishCollectingMetrics = { (_, _, _) in taskDidFinishCollecting.fulfill() }
+        eventMonitor.requestDidCreateInitialURLRequest = { (_, _) in didCreateInitialURLRequest.fulfill() }
         eventMonitor.requestDidCreateURLRequest = { (_, _) in didCreateURLRequest.fulfill() }
         eventMonitor.requestDidCreateTask = { (_, _) in didCreateTask.fulfill() }
         eventMonitor.requestDidGatherMetrics = { (_, _) in didGatherMetrics.fulfill() }

+ 6 - 2
Tests/NSLoggingEventMonitor.swift

@@ -100,8 +100,8 @@ public final class NSLoggingEventMonitor: EventMonitor {
         NSLog("URLSession: \(session), downloadTask: \(downloadTask), didFinishDownloadingTo: \(location)")
     }
 
-    public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {
-        NSLog("Request: \(request) didCreateURLRequest: \(urlRequest)")
+    public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {
+        NSLog("Request: \(request) didCreateInitialURLRequest: \(urlRequest)")
     }
 
     public func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) {
@@ -116,6 +116,10 @@ public final class NSLoggingEventMonitor: EventMonitor {
         NSLog("Request: \(request) didFailToAdaptURLRequest \(initialRequest) withError \(error)")
     }
 
+    public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {
+        NSLog("Request: \(request) didCreateURLRequest: \(urlRequest)")
+    }
+
     public func request(_ request: Request, didCreateTask task: URLSessionTask) {
         NSLog("Request: \(request) didCreateTask \(task)")
     }

+ 162 - 83
Tests/RequestTests.swift

@@ -532,7 +532,7 @@ class RequestResponseTestCase: BaseTestCase {
         let taskDidFinishCollecting = expectation(description: "taskDidFinishCollecting should fire")
         let didReceiveData = expectation(description: "didReceiveData should fire")
         let willCacheResponse = expectation(description: "willCacheResponse should fire")
-        let didCreateURLRequest = expectation(description: "didCreateURLRequest should fire")
+        let didCreateURLRequest = expectation(description: "didCreateInitialURLRequest should fire")
         let didCreateTask = expectation(description: "didCreateTask should fire")
         let didGatherMetrics = expectation(description: "didGatherMetrics should fire")
         let didComplete = expectation(description: "didComplete should fire")
@@ -553,7 +553,7 @@ class RequestResponseTestCase: BaseTestCase {
             didReceiveData.fulfill()
         }
         eventMonitor.dataTaskWillCacheResponse = { (_, _, _) in willCacheResponse.fulfill() }
-        eventMonitor.requestDidCreateURLRequest = { (_, _) in didCreateURLRequest.fulfill() }
+        eventMonitor.requestDidCreateInitialURLRequest = { (_, _) in didCreateURLRequest.fulfill() }
         eventMonitor.requestDidCreateTask = { (_, _) in didCreateTask.fulfill() }
         eventMonitor.requestDidGatherMetrics = { (_, _) in didGatherMetrics.fulfill() }
         eventMonitor.requestDidCompleteTaskWithError = { (_, _, _) in didComplete.fulfill() }
@@ -579,7 +579,7 @@ class RequestResponseTestCase: BaseTestCase {
         let session = Session(startRequestsImmediately: false, eventMonitors: [eventMonitor])
 
         let taskDidFinishCollecting = expectation(description: "taskDidFinishCollecting should fire")
-        let didCreateURLRequest = expectation(description: "didCreateURLRequest should fire")
+        let didCreateURLRequest = expectation(description: "didCreateInitialURLRequest should fire")
         let didCreateTask = expectation(description: "didCreateTask should fire")
         let didGatherMetrics = expectation(description: "didGatherMetrics should fire")
         let didComplete = expectation(description: "didComplete should fire")
@@ -592,7 +592,7 @@ class RequestResponseTestCase: BaseTestCase {
         let responseHandler = expectation(description: "responseHandler should fire")
 
         eventMonitor.taskDidFinishCollectingMetrics = { (_, _, _) in taskDidFinishCollecting.fulfill() }
-        eventMonitor.requestDidCreateURLRequest = { (_, _) in didCreateURLRequest.fulfill() }
+        eventMonitor.requestDidCreateInitialURLRequest = { (_, _) in didCreateURLRequest.fulfill() }
         eventMonitor.requestDidCreateTask = { (_, _) in didCreateTask.fulfill() }
         eventMonitor.requestDidGatherMetrics = { (_, _) in didGatherMetrics.fulfill() }
         eventMonitor.requestDidCompleteTaskWithError = { (_, _, _) in didComplete.fulfill() }
@@ -747,7 +747,7 @@ class RequestDescriptionTestCase: BaseTestCase {
 
 // MARK: -
 
-class RequestDebugDescriptionTestCase: BaseTestCase {
+final class RequestCURLDescriptionTestCase: BaseTestCase {
     // MARK: Properties
 
     let manager: Session = {
@@ -798,86 +798,157 @@ class RequestDebugDescriptionTestCase: BaseTestCase {
 
     // MARK: Tests
 
-    func testGETRequestDebugDescription() {
+    func testGETRequestCURLDescription() {
         // Given
         let urlString = "https://httpbin.org/get"
         let expectation = self.expectation(description: "request should complete")
+        var components: [String]?
 
         // When
-        let request = manager.request(urlString).response { _ in expectation.fulfill() }
+        manager.request(urlString).cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            expectation.fulfill()
+        }
 
         waitForExpectations(timeout: timeout, handler: nil)
 
-        let components = cURLCommandComponents(for: request)
+        // Then
+        XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
+        XCTAssertTrue(components?.contains("-X") == true)
+        XCTAssertEqual(components?.last, "\"\(urlString)\"")
+    }
+
+    func testGETRequestCURLDescriptionSynchronous() {
+        // Given
+        let urlString = "https://httpbin.org/get"
+        let expectation = self.expectation(description: "request should complete")
+        var components: [String]?
+        var syncComponents: [String]?
+
+        // When
+        let request = manager.request(urlString)
+        request.cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            syncComponents = self.cURLCommandComponents(from:request.cURLDescription())
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
 
         // Then
-        XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
-        XCTAssertTrue(components.contains("-X"))
-        XCTAssertEqual(components.last, "\"\(urlString)\"")
+        XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
+        XCTAssertTrue(components?.contains("-X") == true)
+        XCTAssertEqual(components?.last, "\"\(urlString)\"")
+        XCTAssertEqual(components, syncComponents)
     }
 
-    func testGETRequestWithJSONHeaderDebugDescription() {
+    func testGETRequestCURLDescriptionCanBeRequestedManyTimes() {
         // Given
         let urlString = "https://httpbin.org/get"
         let expectation = self.expectation(description: "request should complete")
+        var components: [String]?
+        var secondComponents: [String]?
 
         // When
-        let headers: HTTPHeaders = [ "X-Custom-Header": "{\"key\": \"value\"}" ]
-        let request = manager.request(urlString, headers: headers).response { _ in expectation.fulfill() }
+        let request = manager.request(urlString)
+        request.cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            request.cURLDescription {
+                secondComponents = self.cURLCommandComponents(from: $0)
+                expectation.fulfill()
+            }
+        }
+        // Trigger the overwrite behavior.
+        request.cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            request.cURLDescription {
+                secondComponents = self.cURLCommandComponents(from: $0)
+                expectation.fulfill()
+            }
+        }
 
         waitForExpectations(timeout: timeout, handler: nil)
 
         // Then
-        XCTAssertNotNil(request.debugDescription.range(of: "-H \"X-Custom-Header: {\\\"key\\\": \\\"value\\\"}\""))
+        XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
+        XCTAssertTrue(components?.contains("-X") == true)
+        XCTAssertEqual(components?.last, "\"\(urlString)\"")
+        XCTAssertEqual(components, secondComponents)
     }
 
-    func testGETRequestWithDuplicateHeadersDebugDescription() {
+    func testGETRequestWithCustomHeaderCURLDescription() {
         // Given
         let urlString = "https://httpbin.org/get"
         let expectation = self.expectation(description: "request should complete")
+        var cURLDescription: String?
 
         // When
-        let headers: HTTPHeaders = [ "Accept-Language": "en-GB" ]
-        let request = managerWithAcceptLanguageHeader.request(urlString, headers: headers).response { _ in expectation.fulfill() }
+        let headers: HTTPHeaders = ["X-Custom-Header": "{\"key\": \"value\"}"]
+        manager.request(urlString, headers: headers).cURLDescription {
+            cURLDescription = $0
+            expectation.fulfill()
+        }
 
         waitForExpectations(timeout: timeout, handler: nil)
 
-        let components = cURLCommandComponents(for: request)
+        // Then
+        XCTAssertNotNil(cURLDescription?.range(of: "-H \"X-Custom-Header: {\\\"key\\\": \\\"value\\\"}\""))
+    }
+
+    func testGETRequestWithDuplicateHeadersDebugDescription() {
+        // Given
+        let urlString = "https://httpbin.org/get"
+        let expectation = self.expectation(description: "request should complete")
+        var cURLDescription: String?
+        var components: [String]?
+
+        // When
+        let headers: HTTPHeaders = ["Accept-Language": "en-GB"]
+        managerWithAcceptLanguageHeader.request(urlString, headers: headers).cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            cURLDescription = $0
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
 
         // Then
-        XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
-        XCTAssertTrue(components.contains("-X"))
-        XCTAssertEqual(components.last, "\"\(urlString)\"")
+        XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
+        XCTAssertTrue(components?.contains("-X") == true)
+        XCTAssertEqual(components?.last, "\"\(urlString)\"")
 
-        let tokens = request.debugDescription.components(separatedBy: "Accept-Language:")
-        XCTAssertTrue(tokens.count == 2, "command should contain a single Accept-Language header")
+        let acceptLanguageCount = components?.filter { $0.contains("Accept-Language") }.count
+        XCTAssertEqual(acceptLanguageCount, 1, "command should contain a single Accept-Language header")
 
-        XCTAssertNotNil(request.debugDescription.range(of: "-H \"Accept-Language: en-GB\""))
+        XCTAssertNotNil(cURLDescription?.range(of: "-H \"Accept-Language: en-GB\""))
     }
 
-    func testPOSTRequestDebugDescription() {
+    func testPOSTRequestCURLDescription() {
         // Given
         let urlString = "https://httpbin.org/post"
         let expectation = self.expectation(description: "request should complete")
-
+        var components: [String]?
 
         // When
-        let request = manager.request(urlString, method: .post).response { _ in expectation.fulfill() }
+        manager.request(urlString, method: .post).cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            expectation.fulfill()
+        }
 
         waitForExpectations(timeout: timeout, handler: nil)
 
-        let components = cURLCommandComponents(for: request)
-
         // Then
-        XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
-        XCTAssertEqual(components[3..<5], ["-X", "POST"])
-        XCTAssertEqual(components.last, "\"\(urlString)\"")
+        XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
+        XCTAssertEqual(components?[3..<5], ["-X", "POST"])
+        XCTAssertEqual(components?.last, "\"\(urlString)\"")
     }
 
-    func testPOSTRequestWithJSONParametersDebugDescription() {
+    func testPOSTRequestWithJSONParametersCURLDescription() {
         // Given
         let urlString = "https://httpbin.org/post"
         let expectation = self.expectation(description: "request should complete")
+        var cURLDescription: String?
+        var components: [String]?
 
         let parameters = [
             "foo": "bar",
@@ -886,28 +957,28 @@ class RequestDebugDescriptionTestCase: BaseTestCase {
         ]
 
         // When
-        let request = manager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default).response {
-            _ in expectation.fulfill()
+        manager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default).cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            cURLDescription = $0
+            expectation.fulfill()
         }
 
         waitForExpectations(timeout: timeout, handler: nil)
 
-        let components = cURLCommandComponents(for: request)
-
         // Then
-        XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
-        XCTAssertEqual(components[3..<5], ["-X", "POST"])
+        XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
+        XCTAssertEqual(components?[3..<5], ["-X", "POST"])
 
-        XCTAssertNotNil(request.debugDescription.range(of: "-H \"Content-Type: application/json\""))
-        XCTAssertNotNil(request.debugDescription.range(of: "-d \"{"))
-        XCTAssertNotNil(request.debugDescription.range(of: "\\\"f'oo\\\":\\\"ba'r\\\""))
-        XCTAssertNotNil(request.debugDescription.range(of: "\\\"fo\\\\\\\"o\\\":\\\"b\\\\\\\"ar\\\""))
-        XCTAssertNotNil(request.debugDescription.range(of: "\\\"foo\\\":\\\"bar\\"))
+        XCTAssertNotNil(cURLDescription?.range(of: "-H \"Content-Type: application/json\""))
+        XCTAssertNotNil(cURLDescription?.range(of: "-d \"{"))
+        XCTAssertNotNil(cURLDescription?.range(of: "\\\"f'oo\\\":\\\"ba'r\\\""))
+        XCTAssertNotNil(cURLDescription?.range(of: "\\\"fo\\\\\\\"o\\\":\\\"b\\\\\\\"ar\\\""))
+        XCTAssertNotNil(cURLDescription?.range(of: "\\\"foo\\\":\\\"bar\\"))
 
-        XCTAssertEqual(components.last, "\"\(urlString)\"")
+        XCTAssertEqual(components?.last, "\"\(urlString)\"")
     }
 
-    func testPOSTRequestWithCookieDebugDescription() {
+    func testPOSTRequestWithCookieCURLDescription() {
         // Given
         let urlString = "https://httpbin.org/post"
 
@@ -921,23 +992,24 @@ class RequestDebugDescriptionTestCase: BaseTestCase {
         let cookie = HTTPCookie(properties: properties)!
         let cookieManager = managerWithCookie(cookie)
         let expectation = self.expectation(description: "request should complete")
-
+        var components: [String]?
 
         // When
-        let request = cookieManager.request(urlString, method: .post).response { _ in expectation.fulfill() }
+        cookieManager.request(urlString, method: .post).cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            expectation.fulfill()
+        }
 
         waitForExpectations(timeout: timeout, handler: nil)
 
-        let components = cURLCommandComponents(for: request)
-
         // Then
-        XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
-        XCTAssertEqual(components[3..<5], ["-X", "POST"])
-        XCTAssertEqual(components.last, "\"\(urlString)\"")
-        XCTAssertEqual(components[5..<6], ["-b"])
+        XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
+        XCTAssertEqual(components?[3..<5], ["-X", "POST"])
+        XCTAssertEqual(components?.last, "\"\(urlString)\"")
+        XCTAssertEqual(components?[5..<6], ["-b"])
     }
 
-    func testPOSTRequestWithCookiesDisabledDebugDescription() {
+    func testPOSTRequestWithCookiesDisabledCURLDescriptionHasNoCookies() {
         // Given
         let urlString = "https://httpbin.org/post"
 
@@ -950,67 +1022,74 @@ class RequestDebugDescriptionTestCase: BaseTestCase {
 
         let cookie = HTTPCookie(properties: properties)!
         managerDisallowingCookies.session.configuration.httpCookieStorage?.setCookie(cookie)
+        let expectation = self.expectation(description: "request should complete")
+        var components: [String]?
 
         // When
-        let request = managerDisallowingCookies.request(urlString, method: .post)
-        let components = cURLCommandComponents(for: request)
+        managerDisallowingCookies.request(urlString, method: .post).cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout, handler: nil)
 
         // Then
-        let cookieComponents = components.filter { $0 == "-b" }
-        XCTAssertTrue(cookieComponents.isEmpty)
+        let cookieComponents = components?.filter { $0 == "-b" }
+        XCTAssertTrue(cookieComponents?.isEmpty == true)
     }
 
-    func testMultipartFormDataRequestWithDuplicateHeadersDebugDescription() {
+    func testMultipartFormDataRequestWithDuplicateHeadersCURLDescriptionHasOneContentTypeHeader() {
         // Given
         let urlString = "https://httpbin.org/post"
         let japaneseData = Data("日本語".utf8)
         let expectation = self.expectation(description: "multipart form data encoding should succeed")
+        var cURLDescription: String?
+        var components: [String]?
 
         // When
-        let request = managerWithContentTypeHeader.upload(multipartFormData: { (data) in
+        managerWithContentTypeHeader.upload(multipartFormData: { (data) in
             data.append(japaneseData, withName: "japanese")
-        }, to: urlString)
-            .response { _ in
-                expectation.fulfill()
-            }
+        }, to: urlString).cURLDescription {
+            components = self.cURLCommandComponents(from: $0)
+            cURLDescription = $0
+            expectation.fulfill()
+        }
 
         waitForExpectations(timeout: timeout, handler: nil)
 
-        let components = cURLCommandComponents(for: request)
-
         // Then
-        XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
-        XCTAssertTrue(components.contains("-X"))
-        XCTAssertEqual(components.last, "\"\(urlString)\"")
+        XCTAssertEqual(components?[0..<3], ["$", "curl", "-v"])
+        XCTAssertTrue(components?.contains("-X") == true)
+        XCTAssertEqual(components?.last, "\"\(urlString)\"")
 
-        let tokens = request.debugDescription.components(separatedBy: "Content-Type:")
-        XCTAssertTrue(tokens.count == 2, "command should contain a single Content-Type header")
+        let contentTypeCount = components?.filter { $0.contains("Content-Type") }.count
+        XCTAssertEqual(contentTypeCount, 1, "command should contain a single Content-Type header")
 
-        XCTAssertNotNil(request.debugDescription.range(of: "-H \"Content-Type: multipart/form-data;"))
+        XCTAssertNotNil(cURLDescription?.range(of: "-H \"Content-Type: multipart/form-data;"))
     }
 
     func testThatRequestWithInvalidURLDebugDescription() {
         // Given
         let urlString = "invalid_url"
         let expectation = self.expectation(description: "request should complete")
+        var cURLDescription: String?
 
         // When
-        let request = manager.request(urlString).response { _ in expectation.fulfill() }
+        manager.request(urlString).cURLDescription {
+            cURLDescription = $0
+            expectation.fulfill()
+        }
 
         waitForExpectations(timeout: timeout, handler: nil)
 
-        let debugDescription = request.debugDescription
-
         // Then
-        XCTAssertNotNil(debugDescription, "debugDescription should not crash")
+        XCTAssertNotNil(cURLDescription, "debugDescription should not crash")
     }
 
     // MARK: Test Helper Methods
 
-    private func cURLCommandComponents(for request: Request) -> [String] {
-        let whitespaceCharacterSet = CharacterSet.whitespacesAndNewlines
-        return request.debugDescription
-            .components(separatedBy: whitespaceCharacterSet)
-            .filter { $0 != "" && $0 != "\\" }
+    private func cURLCommandComponents(from cURLString: String) -> [String] {
+        return cURLString.components(separatedBy: .whitespacesAndNewlines)
+                         .filter { $0 != "" && $0 != "\\" }
     }
 }

+ 4 - 0
Tests/UploadTests.swift

@@ -656,6 +656,7 @@ final class UploadRequestEventsTestCase: BaseTestCase {
         let session = Session(eventMonitors: [eventMonitor])
 
         let taskDidFinishCollecting = expectation(description: "taskDidFinishCollecting should fire")
+        let didCreateInitialURLRequest = expectation(description: "didCreateInitialURLRequest should fire")
         let didCreateURLRequest = expectation(description: "didCreateURLRequest should fire")
         let didCreateTask = expectation(description: "didCreateTask should fire")
         let didGatherMetrics = expectation(description: "didGatherMetrics should fire")
@@ -668,6 +669,7 @@ final class UploadRequestEventsTestCase: BaseTestCase {
         let responseHandler = expectation(description: "responseHandler should fire")
 
         eventMonitor.taskDidFinishCollectingMetrics = { (_, _, _) in taskDidFinishCollecting.fulfill() }
+        eventMonitor.requestDidCreateInitialURLRequest = { (_, _) in didCreateInitialURLRequest.fulfill() }
         eventMonitor.requestDidCreateURLRequest = { (_, _) in didCreateURLRequest.fulfill() }
         eventMonitor.requestDidCreateTask = { (_, _) in didCreateTask.fulfill() }
         eventMonitor.requestDidGatherMetrics = { (_, _) in didGatherMetrics.fulfill() }
@@ -696,6 +698,7 @@ final class UploadRequestEventsTestCase: BaseTestCase {
         let session = Session(startRequestsImmediately: false, eventMonitors: [eventMonitor])
 
         let taskDidFinishCollecting = expectation(description: "taskDidFinishCollecting should fire")
+        let didCreateInitialURLRequest = expectation(description: "didCreateInitialURLRequest should fire")
         let didCreateURLRequest = expectation(description: "didCreateURLRequest should fire")
         let didCreateTask = expectation(description: "didCreateTask should fire")
         let didGatherMetrics = expectation(description: "didGatherMetrics should fire")
@@ -710,6 +713,7 @@ final class UploadRequestEventsTestCase: BaseTestCase {
         let responseHandler = expectation(description: "responseHandler should fire")
 
         eventMonitor.taskDidFinishCollectingMetrics = { (_, _, _) in taskDidFinishCollecting.fulfill() }
+        eventMonitor.requestDidCreateInitialURLRequest = { (_, _) in didCreateInitialURLRequest.fulfill() }
         eventMonitor.requestDidCreateURLRequest = { (_, _) in didCreateURLRequest.fulfill() }
         eventMonitor.requestDidCreateTask = { (_, _) in didCreateTask.fulfill() }
         eventMonitor.requestDidGatherMetrics = { (_, _) in didGatherMetrics.fulfill() }