From 78fe423176fb46f2c8fcc1727f7be2b85f9c2cb8 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 13:27:03 -0500
Subject: [PATCH 01/12] Add URL session client and tests

---
 Apollo.xcodeproj/project.pbxproj              |  12 ++
 Sources/Apollo/URLSessionClient.swift         | 197 ++++++++++++++++++
 Tests/ApolloTests/HTTPBinAPI.swift            |  43 ++++
 Tests/ApolloTests/URLSessionClientTests.swift | 129 ++++++++++++
 4 files changed, 381 insertions(+)
 create mode 100644 Sources/Apollo/URLSessionClient.swift
 create mode 100644 Tests/ApolloTests/HTTPBinAPI.swift
 create mode 100644 Tests/ApolloTests/URLSessionClientTests.swift

diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj
index 41890cdde3..8a1fa4948d 100644
--- a/Apollo.xcodeproj/project.pbxproj
+++ b/Apollo.xcodeproj/project.pbxproj
@@ -20,6 +20,9 @@
 		9B21FD772422C8CC00998B5C /* TestFileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B21FD762422C8CC00998B5C /* TestFileHelper.swift */; };
 		9B21FD782424305700998B5C /* ExpectedEnumWithDifferentCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F05F2416F80C00E97318 /* ExpectedEnumWithDifferentCases.swift */; };
 		9B21FD792424305E00998B5C /* ExpectedEnumWithSanitizedCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F063241703B200E97318 /* ExpectedEnumWithSanitizedCases.swift */; };
+		9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */; };
+		9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */; };
+		9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */; };
 		9B518C87235F819E004C426D /* CLIDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C85235F8125004C426D /* CLIDownloader.swift */; };
 		9B518C8C235F8B5F004C426D /* ApolloFilePathHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C8A235F8B05004C426D /* ApolloFilePathHelper.swift */; };
 		9B518C8D235F8B9E004C426D /* CLIDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C88235F8AD4004C426D /* CLIDownloaderTests.swift */; };
@@ -365,6 +368,9 @@
 		9B21FD742422C29D00998B5C /* GraphQLFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFileTests.swift; sourceTree = "<group>"; };
 		9B21FD762422C8CC00998B5C /* TestFileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFileHelper.swift; sourceTree = "<group>"; };
 		9B4AA8AD239EFDC9003E1300 /* Apollo-Target-CodegenTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-CodegenTests.xcconfig"; sourceTree = "<group>"; };
+		9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionClient.swift; sourceTree = "<group>"; };
+		9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPBinAPI.swift; sourceTree = "<group>"; };
+		9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionClientTests.swift; sourceTree = "<group>"; };
 		9B518C85235F8125004C426D /* CLIDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIDownloader.swift; sourceTree = "<group>"; };
 		9B518C88235F8AD4004C426D /* CLIDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIDownloaderTests.swift; sourceTree = "<group>"; };
 		9B518C8A235F8B05004C426D /* ApolloFilePathHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloFilePathHelper.swift; sourceTree = "<group>"; };
@@ -727,6 +733,7 @@
 				C3279FC52345233000224790 /* TestCustomRequestCreator.swift */,
 				9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */,
 				9B21FD762422C8CC00998B5C /* TestFileHelper.swift */,
+				9B4F4540244A2A9200C2CF7D /* HTTPBinAPI.swift */,
 			);
 			name = TestHelpers;
 			sourceTree = "<group>";
@@ -1169,6 +1176,7 @@
 				9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */,
 				C338DF1622DD9DE9006AF33E /* RequestCreatorTests.swift */,
 				9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */,
+				9B4F4542244A2AD300C2CF7D /* URLSessionClientTests.swift */,
 				5BB2C0222380836100774170 /* VersionNumberTests.swift */,
 				C304EBD322DDC7B200748F72 /* a.txt */,
 				C35D43BE22DDD3C100BCBABE /* b.txt */,
@@ -1203,6 +1211,7 @@
 				C377CCAA22D7992E00572E03 /* MultipartFormData.swift */,
 				9F69FFA81D42855900E000B1 /* NetworkTransport.swift */,
 				9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */,
+				9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */,
 			);
 			name = Network;
 			sourceTree = "<group>";
@@ -1997,6 +2006,7 @@
 				5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */,
 				9FE941D01E62C771007CDD89 /* Promise.swift in Sources */,
 				9BA1245E22DE116B00BF1D24 /* Result+Helpers.swift in Sources */,
+				9B4F453F244A27B900C2CF7D /* URLSessionClient.swift in Sources */,
 				9FC750631D2A59F600458D91 /* ApolloClient.swift in Sources */,
 				9BA3130E2302BEA5007B7FC5 /* DispatchQueue+Optional.swift in Sources */,
 				9F86B6901E65533D00B885FF /* GraphQLResponseGenerator.swift in Sources */,
@@ -2013,6 +2023,7 @@
 				9FC9A9C81E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift in Sources */,
 				9F91CF8F1F6C0DB2008DD0BE /* MutatingResultsTests.swift in Sources */,
 				F82E62E122BCD223000C311B /* AutomaticPersistedQueriesTests.swift in Sources */,
+				9B4F4543244A2AD300C2CF7D /* URLSessionClientTests.swift in Sources */,
 				9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */,
 				9F533AB31E6C4A4200CBE097 /* BatchedLoadTests.swift in Sources */,
 				C3279FC72345234D00224790 /* TestCustomRequestCreator.swift in Sources */,
@@ -2030,6 +2041,7 @@
 				9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */,
 				9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */,
 				9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */,
+				9B4F4541244A2A9200C2CF7D /* HTTPBinAPI.swift in Sources */,
 				9BF1A94F22CA5784005292C2 /* HTTPTransportTests.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
diff --git a/Sources/Apollo/URLSessionClient.swift b/Sources/Apollo/URLSessionClient.swift
new file mode 100644
index 0000000000..720318e732
--- /dev/null
+++ b/Sources/Apollo/URLSessionClient.swift
@@ -0,0 +1,197 @@
+import Foundation
+
+open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
+  
+  public enum URLSessionClientError: Error {
+    case sessionBecameInvalidWithoutUnderlyingError
+    case dataForTaskNotFound(taskIdentifier: Int)
+  }
+  
+  public typealias Completion = (Result<(Data, HTTPURLResponse?), Error>) -> Void
+  
+  private var completionBlocks = [Int: Completion]()
+  private var datas = [Int: Data]()
+  private var responses = [Int: HTTPURLResponse]()
+  
+  open private(set) var session: URLSession!
+  
+  public init(sessionConfiguration: URLSessionConfiguration = .default,
+              callbackQueue: OperationQueue? = .main) {
+    super.init()
+    self.session = URLSession(configuration: sessionConfiguration,
+                              delegate: self,
+                              delegateQueue: callbackQueue)
+  }
+  
+  private func clearTask(with identifier: Int) {
+    self.completionBlocks.removeValue(forKey: identifier)
+    self.datas.removeValue(forKey: identifier)
+    self.responses.removeValue(forKey: identifier)
+  }
+  
+  private func clearAllTasks() {
+    self.completionBlocks.removeAll()
+    self.datas.removeAll()
+    self.responses.removeAll()
+  }
+  
+  @discardableResult
+  open func sendRequest(_ request: URLRequest, completion: @escaping Completion) -> URLSessionTask {
+    let dataTask = self.session.dataTask(with: request)
+    self.completionBlocks[dataTask.taskIdentifier] = completion
+    self.datas[dataTask.taskIdentifier] = Data()
+    dataTask.resume()
+    
+    return dataTask
+  }
+  
+  open func cancel(task: URLSessionTask) {
+    let taskID = task.taskIdentifier
+    self.clearTask(with: taskID)
+    task.cancel()
+  }
+  
+  // MARK: - URLSessionDelegate
+  
+  open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
+    let finalError = error ?? URLSessionClientError.sessionBecameInvalidWithoutUnderlyingError
+    for block in completionBlocks.values {
+      block(.failure(finalError))
+    }
+    
+    self.clearAllTasks()
+  }
+  
+  @available(OSX 10.12, iOS 10.0, *)
+  open func urlSession(_ session: URLSession,
+                       task: URLSessionTask,
+                       didFinishCollecting metrics: URLSessionTaskMetrics) {
+    // No default implementation
+  }
+  
+  open func urlSession(_ session: URLSession,
+                       didReceive challenge: URLAuthenticationChallenge,
+                       completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+    completionHandler(.performDefaultHandling, nil)
+  }
+  
+  
+  #if os(iOS) || os(tvOS) || os(watchOS)
+  open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
+    // No default implementation
+  }
+  #endif
+  
+  // MARK: - NSURLSessionTaskDelegate
+  
+  open func urlSession(_ session: URLSession,
+                       task: URLSessionTask,
+                       didReceive challenge: URLAuthenticationChallenge,
+                       completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+    completionHandler(.performDefaultHandling, nil)
+  }
+  
+  open func urlSession(_ session: URLSession,
+                       taskIsWaitingForConnectivity task: URLSessionTask) {
+    // No default implementation
+  }
+  
+  open func urlSession(_ session: URLSession,
+                       task: URLSessionTask,
+                       didCompleteWithError error: Error?) {
+    defer {
+      self.clearTask(with: task.taskIdentifier)
+    }
+    
+    guard let completion = self.completionBlocks.removeValue(forKey: task.taskIdentifier) else {
+      // No completion block, the task has likely been cancelled. Bail out.
+      return
+    }
+    
+    
+    if let finalError = error {
+      completion(.failure(finalError))
+    } else {
+      let taskIdentifier = task.taskIdentifier
+      guard let data = self.datas[taskIdentifier] else {
+        // Data is immediately created for a task on creation, so if it's not there, something's gone wrong.
+        completion(.failure(URLSessionClientError.dataForTaskNotFound(taskIdentifier: taskIdentifier)))
+        return
+      }
+      
+      let response = self.responses[taskIdentifier]
+      completion(.success((data, response)))
+    }
+  }
+  
+  open func urlSession(_ session: URLSession,
+                       task: URLSessionTask,
+                       needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
+    completionHandler(nil)
+  }
+  
+  open func urlSession(_ session: URLSession,
+                       task: URLSessionTask,
+                       didSendBodyData bytesSent: Int64,
+                       totalBytesSent: Int64,
+                       totalBytesExpectedToSend: Int64) {
+    // No default implementation
+  }
+  
+  @available(iOS 11.0, OSXApplicationExtension 10.13, *)
+  public func urlSession(_ session: URLSession,
+                         task: URLSessionTask,
+                         willBeginDelayedRequest request: URLRequest,
+                         completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
+    completionHandler(.continueLoading, request)
+  }
+  
+  public func urlSession(_ session: URLSession,
+                         task: URLSessionTask,
+                         willPerformHTTPRedirection response: HTTPURLResponse,
+                         newRequest request: URLRequest,
+                         completionHandler: @escaping (URLRequest?) -> Void) {
+    completionHandler(request)
+  }
+  
+  // MARK: - URLSessionDataDelegate
+  
+  open func urlSession(_ session: URLSession,
+                       dataTask: URLSessionDataTask,
+                       didReceive data: Data) {
+    self.datas[dataTask.taskIdentifier]?.append(data)
+  }
+  
+  @available(iOS 9.0, OSXApplicationExtension 10.11, *)
+  open func urlSession(_ session: URLSession,
+                       dataTask: URLSessionDataTask,
+                       didBecome streamTask: URLSessionStreamTask) {
+    // No default implementation
+  }
+  
+  open func urlSession(_ session: URLSession,
+                       dataTask: URLSessionDataTask,
+                       didBecome downloadTask: URLSessionDownloadTask) {
+    // No default implementation
+  }
+  
+  open func urlSession(_ session: URLSession,
+                       dataTask: URLSessionDataTask,
+                       willCacheResponse proposedResponse: CachedURLResponse,
+                       completionHandler: @escaping (CachedURLResponse?) -> Void) {
+    completionHandler(proposedResponse)
+  }
+  
+  open func urlSession(_ session: URLSession,
+                       dataTask: URLSessionDataTask,
+                       didReceive response: URLResponse,
+                       completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
+    defer {
+      completionHandler(.allow)
+    }
+    
+    if let httpResponse = response as? HTTPURLResponse {
+      self.responses[dataTask.taskIdentifier] = httpResponse
+    }
+  }
+}
diff --git a/Tests/ApolloTests/HTTPBinAPI.swift b/Tests/ApolloTests/HTTPBinAPI.swift
new file mode 100644
index 0000000000..77eb1921ea
--- /dev/null
+++ b/Tests/ApolloTests/HTTPBinAPI.swift
@@ -0,0 +1,43 @@
+import Foundation
+
+enum HTTPBinAPI {
+    static let baseURL = URL(string: "https://httpbin.org/")!
+    enum Endpoint {
+        case bytes(count: Int)
+        case get
+        case headers
+        case image
+        case post
+        
+        var toString: String {
+            
+            switch self {
+            case .bytes(let count):
+                return "bytes/\(count)"
+            case .get:
+                return "get"
+            case .headers:
+                return "headers"
+            case .image:
+                return "image/jpeg"
+            case .post:
+                return "post"
+            }
+        }
+        
+        var toURL: URL {
+            HTTPBinAPI.baseURL.appendingPathComponent(self.toString)
+        }
+    }
+}
+
+struct HTTPBinResponse: Codable {
+    
+    let headers: [String: String]
+    let url: String
+    let json: [String: String]?
+    
+    init(data: Data) throws {
+        self = try JSONDecoder().decode(Self.self, from: data)
+    }
+}
diff --git a/Tests/ApolloTests/URLSessionClientTests.swift b/Tests/ApolloTests/URLSessionClientTests.swift
new file mode 100644
index 0000000000..d1beade0a0
--- /dev/null
+++ b/Tests/ApolloTests/URLSessionClientTests.swift
@@ -0,0 +1,129 @@
+import XCTest
+@testable import Apollo
+
+class URLSessionClientLiveTests: XCTestCase {
+  
+  lazy var client = URLSessionClient()
+  
+  private func request(for endpoint: HTTPBinAPI.Endpoint) -> URLRequest {
+    URLRequest(url: endpoint.toURL,
+               cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData,
+               timeoutInterval: 30)
+  }
+  
+  func testBasicGet() {
+    let request = self.request(for: .get)
+    let expectation = self.expectation(description: "Basic GET request completed")
+    self.client.sendRequest(request) { result in
+      defer {
+        expectation.fulfill()
+      }
+      
+      switch result {
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
+      case .success(let (data, httpResponse)):
+        XCTAssertFalse(data.isEmpty)
+        XCTAssertEqual(request.url, httpResponse?.url)
+      }
+    }
+    
+    self.wait(for: [expectation], timeout: 10)
+  }
+  
+  func testGettingImage() {
+    let request = self.request(for: .image)
+    let expectation = self.expectation(description: "GET request for image completed")
+    self.client.sendRequest(request) { result in
+      defer {
+        expectation.fulfill()
+      }
+      
+      switch result {
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
+      case .success(let (data, httpResponse)):
+        XCTAssertFalse(data.isEmpty)
+        XCTAssertEqual(httpResponse!.allHeaderFields["Content-Type"] as! String, "image/jpeg")
+        let image = UIImage(data: data)
+        XCTAssertNotNil(image)
+        XCTAssertEqual(request.url, httpResponse?.url)
+      }
+    }
+    
+    self.wait(for: [expectation], timeout: 10)
+  }
+  
+  func testGettingBytes() throws {
+    let randomInt = Int.random(in: 1...102_400) // 102400 is max from HTTPBin
+    let request = self.request(for: .bytes(count: randomInt))
+    
+    let expectation = self.expectation(description: "GET request for a random amount of data completed")
+    self.client.sendRequest(request) { result in
+      defer {
+        expectation.fulfill()
+      }
+      
+      switch result {
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
+      case .success(let (data, response)):
+        XCTAssertEqual(data.count,
+                       randomInt,
+                       "Expected \(randomInt) bytes, got \(data.count)")
+        XCTAssertEqual(request.url, response?.url)
+      }
+    }
+    
+    self.wait(for: [expectation], timeout: 10)
+  }
+  
+  func testPostingJSON() throws {
+    let testJSON = ["key": "value"]
+    let data = try JSONSerialization.data(withJSONObject: testJSON, options: .prettyPrinted)
+    
+    var request = self.request(for: .post)
+    request.httpBody = data
+    request.httpMethod = GraphQLHTTPMethod.POST.rawValue
+    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+    
+    let expectation = self.expectation(description: "POST request with JSON completed")
+    self.client.sendRequest(request) { result in
+      defer {
+        expectation.fulfill()
+      }
+      
+      switch result {
+      case .failure(let error):
+        XCTFail("Unexpected error: \(error)")
+      case .success(let (data, httpResponse)):
+        XCTAssertEqual(request.url, httpResponse?.url)
+        
+        do {
+          let parsed = try HTTPBinResponse(data: data)
+          XCTAssertEqual(parsed.json, testJSON)
+        } catch {
+          XCTFail("Unexpected error: \(error)")
+        }
+      }
+    }
+    
+    self.wait(for: [expectation], timeout: 10)
+  }
+  
+  func testCancelling() throws {
+    let request = self.request(for: .bytes(count: 102400)) // 102400 is max from HTTPBin
+    
+    let expectation = self.expectation(description: "Cancelled task completed anyway")
+    expectation.isInverted = true
+    let task = self.client.sendRequest(request) { result in
+      // This shouldn't get hit since we cancel the task immediately
+      expectation.fulfill()
+    }
+    
+    self.client.cancel(task: task)
+    
+    self.wait(for: [expectation], timeout: 10)
+    
+  }
+}

From bb9354affc4966b5eb9a8aea03257fc283cb2428 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 15:52:33 -0500
Subject: [PATCH 02/12] replace URLSession with URLSession client, add raw
 result callback and go to requiring http response

---
 Sources/Apollo/HTTPNetworkTransport.swift | 125 +++++++++-------------
 Sources/Apollo/URLSessionClient.swift     |  41 +++++--
 2 files changed, 82 insertions(+), 84 deletions(-)

diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift
index fdcba682e5..7955075cc3 100644
--- a/Sources/Apollo/HTTPNetworkTransport.swift
+++ b/Sources/Apollo/HTTPNetworkTransport.swift
@@ -103,7 +103,7 @@ public class HTTPNetworkTransport {
   }
   
   let url: URL
-  let session: URLSession
+  let client: URLSessionClient
   let serializationFormat = JSONSerializationFormat.self
   let useGETForQueries: Bool
   let enableAutoPersistedQueries: Bool
@@ -127,14 +127,14 @@ public class HTTPNetworkTransport {
   ///   - enableAutoPersistedQueries: Whether to send persistedQuery extension. QueryDocument will be absent at 1st request, retry with QueryDocument if server respond PersistedQueryNotFound or PersistedQueryNotSupport. Defaults to false.
   ///   - useGETForPersistedQueryRetry: Whether to retry persistedQuery request with HttpGetMethod. Defaults to false.
   public init(url: URL,
-              session: URLSession = .shared,
+              client: URLSessionClient = URLSessionClient(),
               sendOperationIdentifiers: Bool = false,
               useGETForQueries: Bool = false,
               enableAutoPersistedQueries: Bool = false,
               useGETForPersistedQueryRetry: Bool = false,
               requestCreator: RequestCreator = ApolloRequestCreator()) {
     self.url = url
-    self.session = session
+    self.client = client
     self.sendOperationIdentifiers = sendOperationIdentifiers
     self.useGETForQueries = useGETForQueries
     self.enableAutoPersistedQueries = enableAutoPersistedQueries
@@ -155,88 +155,67 @@ public class HTTPNetworkTransport {
       completionHandler(.failure(error))
       return EmptyCancellable()
     }
-
-    let task = session.dataTask(with: request) { [weak self] data, response, error in
+    
+    let task = self.client.sendRequest(request, rawTaskCompletionHandler: { [weak self] data, response, error in
+      self?.rawTaskCompleted(request: request, data: data, response: response, error: error)
+    }, completion: { [weak self] result in
       guard let self = self else {
         // None of the rest of this really matters
         return
       }
-
-      self.rawTaskCompleted(request: request,
-                            data: data,
-                            response: response,
-                            error: error)
-
-      if let receivedError = error {
-        self.handleErrorOrRetry(operation: operation,
-                                files: files,
-                                error: receivedError,
-                                for: request,
-                                response: response,
-                                completionHandler: completionHandler)
-        return
-      }
-
-      guard let httpResponse = response as? HTTPURLResponse else {
-        fatalError("Response should be an HTTPURLResponse")
-      }
-
-      guard httpResponse.isSuccessful else {
-        let unsuccessfulError = GraphQLHTTPResponseError(body: data,
-                                                         response: httpResponse,
-                                                         kind: .errorResponse)
-        self.handleErrorOrRetry(operation: operation,
-                                files: files,
-                                error: unsuccessfulError,
-                                for: request,
-                                response: response,
-                                completionHandler: completionHandler)
-        return
-      }
-
-      guard let data = data else {
-        let error = GraphQLHTTPResponseError(body: nil,
-                                             response: httpResponse,
-                                             kind: .invalidResponse)
+      
+      switch result {
+      case .failure(let error):
         self.handleErrorOrRetry(operation: operation,
                                 files: files,
                                 error: error,
                                 for: request,
-                                response: response,
+                                response: nil,
                                 completionHandler: completionHandler)
-        return
-      }
-
-      do {
-        guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else {
-          throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
+      case .success(let (data, httpResponse)):
+        guard httpResponse.isSuccessful == true else {
+          let unsuccessfulError = GraphQLHTTPResponseError(body: data,
+                                                           response: httpResponse,
+                                                           kind: .errorResponse)
+          self.handleErrorOrRetry(operation: operation,
+                                  files: files,
+                                  error: unsuccessfulError,
+                                  for: request,
+                                  response: httpResponse,
+                                  completionHandler: completionHandler)
+          return
         }
-
-        let graphQLResponse = GraphQLResponse(operation: operation, body: body)
-
-        if let errors = graphQLResponse.parseErrorsOnlyFast() {
-          // Handle specific errors from response
-          self.handleGraphQLErrorsIfNeeded(operation: operation,
-                                           files: files,
-                                           for: request,
-                                           body: body,
-                                           errors: errors,
-                                           completionHandler: completionHandler)
-        } else {
-          completionHandler(.success(graphQLResponse))
+        
+        do {
+          guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else {
+            throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
+          }
+
+          let graphQLResponse = GraphQLResponse(operation: operation, body: body)
+
+          if let errors = graphQLResponse.parseErrorsOnlyFast() {
+            // Handle specific errors from response
+            self.handleGraphQLErrorsIfNeeded(operation: operation,
+                                             files: files,
+                                             for: request,
+                                             body: body,
+                                             errors: errors,
+                                             completionHandler: completionHandler)
+          } else {
+            completionHandler(.success(graphQLResponse))
+          }
+        } catch let parsingError {
+          self.handleErrorOrRetry(operation: operation,
+                                  files: files,
+                                  error: parsingError,
+                                  for: request,
+                                  response: httpResponse,
+                                  completionHandler: completionHandler)
         }
-      } catch let parsingError {
-        self.handleErrorOrRetry(operation: operation,
-                                files: files,
-                                error: parsingError,
-                                for: request,
-                                response: response,
-                                completionHandler: completionHandler)
       }
-    }
-
-    task.resume()
+    })
 
+    // Task is resumed by underlying framework
     return task
   }
 
@@ -485,7 +464,7 @@ extension HTTPNetworkTransport: Equatable {
 
   public static func ==(lhs: HTTPNetworkTransport, rhs: HTTPNetworkTransport) -> Bool {
     return lhs.url == rhs.url
-      && lhs.session == rhs.session
+      && lhs.client == rhs.client
       && lhs.sendOperationIdentifiers == rhs.sendOperationIdentifiers
       && lhs.useGETForQueries == rhs.useGETForQueries
   }
diff --git a/Sources/Apollo/URLSessionClient.swift b/Sources/Apollo/URLSessionClient.swift
index 720318e732..a5b2958adf 100644
--- a/Sources/Apollo/URLSessionClient.swift
+++ b/Sources/Apollo/URLSessionClient.swift
@@ -3,13 +3,16 @@ import Foundation
 open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
   
   public enum URLSessionClientError: Error {
+    case noHTTPResponse(url: URL?)
     case sessionBecameInvalidWithoutUnderlyingError
     case dataForTaskNotFound(taskIdentifier: Int)
   }
   
-  public typealias Completion = (Result<(Data, HTTPURLResponse?), Error>) -> Void
+  public typealias RawCompletion = (Data?, URLResponse?, Error?) -> Void
+  public typealias Completion = (Result<(Data, HTTPURLResponse), Error>) -> Void
   
   private var completionBlocks = [Int: Completion]()
+  private var rawCompletions = [Int: RawCompletion]()
   private var datas = [Int: Data]()
   private var responses = [Int: HTTPURLResponse]()
   
@@ -23,21 +26,26 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
                               delegateQueue: callbackQueue)
   }
   
-  private func clearTask(with identifier: Int) {
+  open func clearTask(with identifier: Int) {
+    self.rawCompletions.removeValue(forKey: identifier)
     self.completionBlocks.removeValue(forKey: identifier)
     self.datas.removeValue(forKey: identifier)
     self.responses.removeValue(forKey: identifier)
   }
   
-  private func clearAllTasks() {
+  open func clearAllTasks() {
+    self.rawCompletions.removeAll()
     self.completionBlocks.removeAll()
     self.datas.removeAll()
     self.responses.removeAll()
   }
   
   @discardableResult
-  open func sendRequest(_ request: URLRequest, completion: @escaping Completion) -> URLSessionTask {
+  open func sendRequest(_ request: URLRequest,
+                        rawTaskCompletionHandler: @escaping RawCompletion,
+                        completion: @escaping Completion) -> URLSessionTask {
     let dataTask = self.session.dataTask(with: request)
+    self.rawCompletions[dataTask.taskIdentifier] = rawTaskCompletionHandler
     self.completionBlocks[dataTask.taskIdentifier] = completion
     self.datas[dataTask.taskIdentifier] = Data()
     dataTask.resume()
@@ -99,28 +107,39 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
   open func urlSession(_ session: URLSession,
                        task: URLSessionTask,
                        didCompleteWithError error: Error?) {
+    let taskIdentifier = task.taskIdentifier
     defer {
-      self.clearTask(with: task.taskIdentifier)
+      self.clearTask(with: taskIdentifier)
     }
     
-    guard let completion = self.completionBlocks.removeValue(forKey: task.taskIdentifier) else {
-      // No completion block, the task has likely been cancelled. Bail out.
+    
+    guard
+      let rawCompletion = self.rawCompletions.removeValue(forKey: taskIdentifier),
+      let completion = self.completionBlocks.removeValue(forKey: taskIdentifier) else {
+      // No completion blocks, the task has likely been cancelled. Bail out.
       return
     }
     
+    let data = self.datas[taskIdentifier]
+    let response = self.responses[taskIdentifier]
+    
+    rawCompletion(data, response, error)
     
     if let finalError = error {
       completion(.failure(finalError))
     } else {
-      let taskIdentifier = task.taskIdentifier
-      guard let data = self.datas[taskIdentifier] else {
+      guard let finalData = data else {
         // Data is immediately created for a task on creation, so if it's not there, something's gone wrong.
         completion(.failure(URLSessionClientError.dataForTaskNotFound(taskIdentifier: taskIdentifier)))
         return
       }
       
-      let response = self.responses[taskIdentifier]
-      completion(.success((data, response)))
+      guard let finalResponse = response else {
+        completion(.failure(URLSessionClientError.noHTTPResponse(url: task.currentRequest?.url)))
+        return
+      }
+      
+      completion(.success((finalData, finalResponse)))
     }
   }
   

From 6beec62bf784fdcb62df3208267e7cce661c8332 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 16:23:08 -0500
Subject: [PATCH 03/12] make raw completion optional, better error handling

---
 Sources/Apollo/URLSessionClient.swift | 26 ++++++++++++++------------
 1 file changed, 14 insertions(+), 12 deletions(-)

diff --git a/Sources/Apollo/URLSessionClient.swift b/Sources/Apollo/URLSessionClient.swift
index a5b2958adf..409cd91f51 100644
--- a/Sources/Apollo/URLSessionClient.swift
+++ b/Sources/Apollo/URLSessionClient.swift
@@ -3,9 +3,9 @@ import Foundation
 open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
   
   public enum URLSessionClientError: Error {
-    case noHTTPResponse(url: URL?)
+    case noHTTPResponse(request: URLRequest?)
     case sessionBecameInvalidWithoutUnderlyingError
-    case dataForTaskNotFound(taskIdentifier: Int)
+    case dataForRequestNotFound(request: URLRequest?)
   }
   
   public typealias RawCompletion = (Data?, URLResponse?, Error?) -> Void
@@ -42,10 +42,13 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
   
   @discardableResult
   open func sendRequest(_ request: URLRequest,
-                        rawTaskCompletionHandler: @escaping RawCompletion,
+                        rawTaskCompletionHandler: RawCompletion? = nil,
                         completion: @escaping Completion) -> URLSessionTask {
     let dataTask = self.session.dataTask(with: request)
-    self.rawCompletions[dataTask.taskIdentifier] = rawTaskCompletionHandler
+    if let rawCompletion = rawTaskCompletionHandler {
+      self.rawCompletions[dataTask.taskIdentifier] = rawCompletion
+    }
+    
     self.completionBlocks[dataTask.taskIdentifier] = completion
     self.datas[dataTask.taskIdentifier] = Data()
     dataTask.resume()
@@ -112,10 +115,7 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
       self.clearTask(with: taskIdentifier)
     }
     
-    
-    guard
-      let rawCompletion = self.rawCompletions.removeValue(forKey: taskIdentifier),
-      let completion = self.completionBlocks.removeValue(forKey: taskIdentifier) else {
+    guard let completion = self.completionBlocks.removeValue(forKey: taskIdentifier) else {
       // No completion blocks, the task has likely been cancelled. Bail out.
       return
     }
@@ -123,19 +123,21 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     let data = self.datas[taskIdentifier]
     let response = self.responses[taskIdentifier]
     
-    rawCompletion(data, response, error)
-    
+    if let rawCompletion = self.rawCompletions.removeValue(forKey: taskIdentifier) {
+      rawCompletion(data, response, error)
+    }
+      
     if let finalError = error {
       completion(.failure(finalError))
     } else {
       guard let finalData = data else {
         // Data is immediately created for a task on creation, so if it's not there, something's gone wrong.
-        completion(.failure(URLSessionClientError.dataForTaskNotFound(taskIdentifier: taskIdentifier)))
+        completion(.failure(URLSessionClientError.dataForRequestNotFound(request: task.originalRequest)))
         return
       }
       
       guard let finalResponse = response else {
-        completion(.failure(URLSessionClientError.noHTTPResponse(url: task.currentRequest?.url)))
+        completion(.failure(URLSessionClientError.noHTTPResponse(request: task.originalRequest)))
         return
       }
       

From c55c4d2b54f0b2a8eeff6bcc278b77a872128bdb Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 16:23:33 -0500
Subject: [PATCH 04/12] fix annotations and make a couple methods subclassable

---
 Sources/Apollo/URLSessionClient.swift | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/Sources/Apollo/URLSessionClient.swift b/Sources/Apollo/URLSessionClient.swift
index 409cd91f51..22ff4e2fb3 100644
--- a/Sources/Apollo/URLSessionClient.swift
+++ b/Sources/Apollo/URLSessionClient.swift
@@ -159,19 +159,19 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     // No default implementation
   }
   
-  @available(iOS 11.0, OSXApplicationExtension 10.13, *)
-  public func urlSession(_ session: URLSession,
-                         task: URLSessionTask,
-                         willBeginDelayedRequest request: URLRequest,
-                         completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
+  @available(iOS 11.0, OSXApplicationExtension 10.13, OSX 10.13, *)
+  open func urlSession(_ session: URLSession,
+                       task: URLSessionTask,
+                       willBeginDelayedRequest request: URLRequest,
+                       completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
     completionHandler(.continueLoading, request)
   }
   
-  public func urlSession(_ session: URLSession,
-                         task: URLSessionTask,
-                         willPerformHTTPRedirection response: HTTPURLResponse,
-                         newRequest request: URLRequest,
-                         completionHandler: @escaping (URLRequest?) -> Void) {
+  open func urlSession(_ session: URLSession,
+                       task: URLSessionTask,
+                       willPerformHTTPRedirection response: HTTPURLResponse,
+                       newRequest request: URLRequest,
+                       completionHandler: @escaping (URLRequest?) -> Void) {
     completionHandler(request)
   }
   

From 29cddc5a836c57e495cb16c46f5a4ae8dbfffac0 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 16:24:00 -0500
Subject: [PATCH 05/12] replace `MockURLSession` with `MockURLSessionClient`
 subclass.

---
 .../ApolloTestSupport/MockURLSession.swift    | 37 ++++++++++++++-----
 1 file changed, 27 insertions(+), 10 deletions(-)

diff --git a/Sources/ApolloTestSupport/MockURLSession.swift b/Sources/ApolloTestSupport/MockURLSession.swift
index 1ab1399759..a21b03a96b 100644
--- a/Sources/ApolloTestSupport/MockURLSession.swift
+++ b/Sources/ApolloTestSupport/MockURLSession.swift
@@ -6,25 +6,42 @@
 //
 
 import Foundation
+import Apollo
+
+public final class MockURLSessionClient: URLSessionClient {
 
-public final class MockURLSession: URLSession {
   public private (set) var lastRequest: URLRequest?
 
   public var data: Data?
   public var response: HTTPURLResponse?
   public var error: Error?
 
-  override public func dataTask(with request: URLRequest) -> URLSessionDataTask {
-    lastRequest = request
-    return URLSessionDataTaskMock()
-  }
+  public override func sendRequest(_ request: URLRequest,
+                                   rawTaskCompletionHandler: URLSessionClient.RawCompletion? = nil,
+                                   completion: @escaping URLSessionClient.Completion) -> URLSessionTask {
+    self.lastRequest = request
+    rawTaskCompletionHandler?(self.data, self.response, self.error)
+    
   
-  override public func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
-    lastRequest = request
-    if let response = response {
-      completionHandler(data, response, error)
+    let mockTask = URLSessionDataTaskMock()
+    
+    if let error = error {
+      completion(.failure(error))
+    } else {
+      guard let data = self.data else {
+        completion(.failure(URLSessionClientError.dataForRequestNotFound(request: request)))
+        return mockTask
+      }
+      
+      guard let response = self.response else {
+        completion(.failure(URLSessionClientError.noHTTPResponse(request: request)))
+        return mockTask
+      }
+      
+      completion(.success((data, response)))
     }
-    return URLSessionDataTaskMock()
+    
+    return mockTask
   }
 }
 

From 0020b13f22ec79a2a3aac03dc56fc11c4854678b Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 16:24:34 -0500
Subject: [PATCH 06/12] update tests to use mock client instead of mock
 session, fix a couple things that don't need to be unwrapped anymore

---
 .../AutomaticPersistedQueriesTests.swift      | 66 +++++++++----------
 Tests/ApolloTests/HTTPTransportTests.swift    | 30 +++++----
 Tests/ApolloTests/URLSessionClientTests.swift | 21 +++---
 3 files changed, 62 insertions(+), 55 deletions(-)

diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift
index 3f7867fe7f..3c6f08b9be 100644
--- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift
+++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift
@@ -230,12 +230,12 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   // MARK: - Tests
   
   func testRequestBody() throws {
-    let mockSession = MockURLSession()
-    let network = HTTPNetworkTransport(url: URL(string: endpoint)!, session: mockSession)
+    let mockClient = MockURLSessionClient()
+    let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient)
     let query = HeroNameQuery()
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -247,12 +247,12 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
   
   func testRequestBodyWithVariable() throws {
-    let mockSession = MockURLSession()
-    let network = HTTPNetworkTransport(url: URL(string: endpoint)!, session: mockSession)
+    let mockClient = MockURLSessionClient()
+    let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient)
     let query = HeroNameQuery(episode: .jedi)
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     XCTAssertEqual(request.url?.host, network.url.host)
     XCTAssertEqual(request.httpMethod, "POST")
@@ -264,14 +264,14 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   
   
   func testRequestBodyForAPQsWithVariable() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: URL(string: endpoint)!,
-                                       session: mockSession,
+                                       client: mockClient,
                                        enableAutoPersistedQueries: true)
     let query = HeroNameQuery(episode: .empire)
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
 
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -283,14 +283,14 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
 
   func testMutationRequestBodyForAPQs() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: URL(string: endpoint)!,
-                                       session: mockSession,
+                                       client: mockClient,
                                        enableAutoPersistedQueries: true)
     let mutation = CreateAwesomeReviewMutation()
     let _ = network.send(operation: mutation) { _ in }
 
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
 
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -302,15 +302,15 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
   
   func testQueryStringForAPQsUseGetMethod() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: URL(string: endpoint)!,
-                                       session: mockSession,
+                                       client: mockClient,
                                        enableAutoPersistedQueries: true,
                                        useGETForPersistedQueryRetry: true)
     let query = HeroNameQuery()
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     XCTAssertEqual(request.url?.host, network.url.host)
     
@@ -320,15 +320,15 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
   
   func testQueryStringForAPQsUseGetMethodWithVariable() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: URL(string: endpoint)!,
-                                       session: mockSession,
+                                       client: mockClient,
                                        enableAutoPersistedQueries: true,
                                        useGETForPersistedQueryRetry: true)
     let query = HeroNameQuery(episode: .empire)
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
 
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -340,14 +340,14 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
   
   func testUseGETForQueriesRequest() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: URL(string: endpoint)!,
-                                       session: mockSession,
+                                       client: mockClient,
                                        useGETForQueries: true)
     let query = HeroNameQuery()
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -359,12 +359,12 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
   
   func testNotUseGETForQueriesRequest() throws {
-    let mockSession = MockURLSession()
-    let network = HTTPNetworkTransport(url: URL(string: endpoint)!, session: mockSession)
+    let mockClient = MockURLSessionClient()
+    let network = HTTPNetworkTransport(url: URL(string: endpoint)!, client: mockClient)
     let query = HeroNameQuery()
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -376,14 +376,14 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
   
   func testNotUseGETForQueriesAPQsRequest() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: URL(string: endpoint)!,
-                                       session: mockSession,
+                                       client: mockClient,
                                        enableAutoPersistedQueries: true)
     let query = HeroNameQuery(episode: .empire)
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
 
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -395,15 +395,15 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
   
   func testUseGETForQueriesAPQsRequest() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: URL(string: endpoint)!,
-                                       session: mockSession,
+                                       client: mockClient,
                                        useGETForQueries: true,
                                        enableAutoPersistedQueries: true)
     let query = HeroNameQuery(episode: .empire)
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -415,15 +415,15 @@ class AutomaticPersistedQueriesTests: XCTestCase {
   }
   
   func testNotUseGETForQueriesAPQsGETRequest() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: URL(string: endpoint)!,
-                                       session: mockSession,
+                                       client: mockClient,
                                        enableAutoPersistedQueries: true,
                                        useGETForPersistedQueryRetry: true)
     let query = HeroNameQuery(episode: .empire)
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     XCTAssertEqual(request.url?.host, network.url.host)
     XCTAssertEqual(request.httpMethod, "GET")
diff --git a/Tests/ApolloTests/HTTPTransportTests.swift b/Tests/ApolloTests/HTTPTransportTests.swift
index eeb0231d03..3ccc3f0a9b 100644
--- a/Tests/ApolloTests/HTTPTransportTests.swift
+++ b/Tests/ApolloTests/HTTPTransportTests.swift
@@ -260,10 +260,12 @@ class HTTPTransportTests: XCTestCase {
   
   func testEquality() {
     let identicalTransport = HTTPNetworkTransport(url: self.url,
+                                                  client: self.networkTransport.client,
                                                   useGETForQueries: true)
     XCTAssertEqual(self.networkTransport, identicalTransport)
     
-    let nonIdenticalTransport = HTTPNetworkTransport(url: self.url)
+    let nonIdenticalTransport = HTTPNetworkTransport(url: self.url,
+                                                     client: self.networkTransport.client)
     XCTAssertNotEqual(self.networkTransport, nonIdenticalTransport)
   }
 
@@ -274,11 +276,11 @@ class HTTPTransportTests: XCTestCase {
     // TODO: Replace this with once it is codable https://github.com/apollographql/apollo-ios/issues/467
     let body = ["errors": [["message": "Test graphql error"]]]
 
-    let mockSession = MockURLSession()
-    mockSession.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
-    mockSession.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
+    let mockClient = MockURLSessionClient()
+    mockClient.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
+    mockClient.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
     let network = HTTPNetworkTransport(url: url,
-                                       session: mockSession)
+                                       client: mockClient)
     network.delegate = self
     let expectation = self.expectation(description: "Send operation completed")
 
@@ -291,7 +293,7 @@ class HTTPTransportTests: XCTestCase {
       }
     }
 
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -309,11 +311,11 @@ class HTTPTransportTests: XCTestCase {
     // TODO: Replace this with once it is codable https://github.com/apollographql/apollo-ios/issues/467
     let body = ["errors": []]
 
-    let mockSession = MockURLSession()
-    mockSession.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
-    mockSession.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
+    let mockClient = MockURLSessionClient()
+    mockClient.response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
+    mockClient.data = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
     let network = HTTPNetworkTransport(url: url,
-                                       session: mockSession)
+                                       client: mockClient)
     network.delegate = self
     let expectation = self.expectation(description: "Send operation completed")
 
@@ -326,7 +328,7 @@ class HTTPTransportTests: XCTestCase {
       }
     }
 
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
 
     XCTAssertEqual(request.url?.host, network.url.host)
@@ -337,13 +339,13 @@ class HTTPTransportTests: XCTestCase {
   }
   
   func testClientNameAndVersionHeadersAreSent() throws {
-    let mockSession = MockURLSession()
+    let mockClient = MockURLSessionClient()
     let network = HTTPNetworkTransport(url: self.url,
-                                       session: mockSession)
+                                       client: mockClient)
     let query = HeroNameQuery(episode: .empire)
     let _ = network.send(operation: query) { _ in }
     
-    let request = try XCTUnwrap(mockSession.lastRequest,
+    let request = try XCTUnwrap(mockClient.lastRequest,
                                 "last request should not be nil")
     
     let clientName = try XCTUnwrap(request.value(forHTTPHeaderField: HTTPNetworkTransport.headerFieldNameApolloClientName),
diff --git a/Tests/ApolloTests/URLSessionClientTests.swift b/Tests/ApolloTests/URLSessionClientTests.swift
index d1beade0a0..fe1091a1f8 100644
--- a/Tests/ApolloTests/URLSessionClientTests.swift
+++ b/Tests/ApolloTests/URLSessionClientTests.swift
@@ -24,7 +24,7 @@ class URLSessionClientLiveTests: XCTestCase {
         XCTFail("Unexpected error: \(error)")
       case .success(let (data, httpResponse)):
         XCTAssertFalse(data.isEmpty)
-        XCTAssertEqual(request.url, httpResponse?.url)
+        XCTAssertEqual(request.url, httpResponse.url)
       }
     }
     
@@ -44,10 +44,15 @@ class URLSessionClientLiveTests: XCTestCase {
         XCTFail("Unexpected error: \(error)")
       case .success(let (data, httpResponse)):
         XCTAssertFalse(data.isEmpty)
-        XCTAssertEqual(httpResponse!.allHeaderFields["Content-Type"] as! String, "image/jpeg")
-        let image = UIImage(data: data)
-        XCTAssertNotNil(image)
-        XCTAssertEqual(request.url, httpResponse?.url)
+        XCTAssertEqual(httpResponse.allHeaderFields["Content-Type"] as! String, "image/jpeg")
+        #if os(macOS)
+          let image = NSImage(data: data)
+          XCTAssertNotNil(image)
+        #else
+          let image = UIImage(data: data)
+          XCTAssertNotNil(image)
+        #endif
+        XCTAssertEqual(request.url, httpResponse.url)
       }
     }
     
@@ -71,7 +76,7 @@ class URLSessionClientLiveTests: XCTestCase {
         XCTAssertEqual(data.count,
                        randomInt,
                        "Expected \(randomInt) bytes, got \(data.count)")
-        XCTAssertEqual(request.url, response?.url)
+        XCTAssertEqual(request.url, response.url)
       }
     }
     
@@ -97,7 +102,7 @@ class URLSessionClientLiveTests: XCTestCase {
       case .failure(let error):
         XCTFail("Unexpected error: \(error)")
       case .success(let (data, httpResponse)):
-        XCTAssertEqual(request.url, httpResponse?.url)
+        XCTAssertEqual(request.url, httpResponse.url)
         
         do {
           let parsed = try HTTPBinResponse(data: data)
@@ -123,7 +128,7 @@ class URLSessionClientLiveTests: XCTestCase {
     
     self.client.cancel(task: task)
     
-    self.wait(for: [expectation], timeout: 10)
+    self.wait(for: [expectation], timeout: 5)
     
   }
 }

From 08c68dcb51b19c2a6b26c38e7554f8231f4cf919 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 16:48:47 -0500
Subject: [PATCH 07/12] add inline documentation to URLSessionClient

---
 Sources/Apollo/URLSessionClient.swift | 36 ++++++++++++++++++++++++---
 1 file changed, 32 insertions(+), 4 deletions(-)

diff --git a/Sources/Apollo/URLSessionClient.swift b/Sources/Apollo/URLSessionClient.swift
index 22ff4e2fb3..b1ef7b32a9 100644
--- a/Sources/Apollo/URLSessionClient.swift
+++ b/Sources/Apollo/URLSessionClient.swift
@@ -1,5 +1,7 @@
 import Foundation
 
+/// A class to handle URL Session calls that will support background execution,
+/// but still (mostly) use callbacks for its primary method of communication.
 open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
   
   public enum URLSessionClientError: Error {
@@ -8,7 +10,10 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     case dataForRequestNotFound(request: URLRequest?)
   }
   
-  public typealias RawCompletion = (Data?, URLResponse?, Error?) -> Void
+  /// A completion block to be called when the raw task has completed, with the raw information from the session
+  public typealias RawCompletion = (Data?, HTTPURLResponse?, Error?) -> Void
+  
+  /// A completion block returning a result. On `.success` it will contain a tuple with non-nil `Data` and its corresponding `HTTPURLResponse`. On `.failure` it will contain an error.
   public typealias Completion = (Result<(Data, HTTPURLResponse), Error>) -> Void
   
   private var completionBlocks = [Int: Completion]()
@@ -16,8 +21,14 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
   private var datas = [Int: Data]()
   private var responses = [Int: HTTPURLResponse]()
   
+  /// The raw URLSession being used for this client
   open private(set) var session: URLSession!
   
+  /// Designated initializer.
+  ///
+  /// - Parameters:
+  ///   - sessionConfiguration: The `URLSessionConfiguration` to use to set up the URL session.
+  ///   - callbackQueue: [optional] The `OperationQueue` to tell the URL session to call back to this class on, which will in turn call back to your class. Defaults to `.main`.
   public init(sessionConfiguration: URLSessionConfiguration = .default,
               callbackQueue: OperationQueue? = .main) {
     super.init()
@@ -26,6 +37,9 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
                               delegateQueue: callbackQueue)
   }
   
+  /// Clears underlying dictionaries of any data related to a particular task identifier.
+  ///
+  /// - Parameter identifier: The identifier of the task to clear.
   open func clearTask(with identifier: Int) {
     self.rawCompletions.removeValue(forKey: identifier)
     self.completionBlocks.removeValue(forKey: identifier)
@@ -33,6 +47,9 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     self.responses.removeValue(forKey: identifier)
   }
   
+  /// Clears underlying dictionaries of any data related to all tasks.
+  ///
+  /// Mostly useful for cleanup and/or after invalidation of the `URLSession`.
   open func clearAllTasks() {
     self.rawCompletions.removeAll()
     self.completionBlocks.removeAll()
@@ -40,6 +57,14 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     self.responses.removeAll()
   }
   
+  /// The main method to perform a request.
+  ///
+  /// - Parameters:
+  ///   - request: The request to perform.
+  ///   - rawTaskCompletionHandler: [optional] A completion handler to call once the raw task is done, so if an Error requires access to the headers, the user can still access these.
+  ///   - completion: A completion handler to call when the task has either completed successfully or failed.
+  ///
+  /// - Returns: The created URLSesssion task, already resumed, because nobody ever remembers to call `resume()`.
   @discardableResult
   open func sendRequest(_ request: URLRequest,
                         rawTaskCompletionHandler: RawCompletion? = nil,
@@ -56,9 +81,13 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     return dataTask
   }
   
+  /// Cancels a given task and clears out its underlying data.
+  ///
+  /// NOTE: You will not receive any kind of "This was cancelled" error when this is called.
+  ///
+  /// - Parameter task: The task you wish to cancel.
   open func cancel(task: URLSessionTask) {
-    let taskID = task.taskIdentifier
-    self.clearTask(with: taskID)
+    self.clearTask(with: task.taskIdentifier)
     task.cancel()
   }
   
@@ -86,7 +115,6 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     completionHandler(.performDefaultHandling, nil)
   }
   
-  
   #if os(iOS) || os(tvOS) || os(watchOS)
   open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
     // No default implementation

From b4df0a7ca3fbc5952ec11146b03a920514bbaac6 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 16:50:36 -0500
Subject: [PATCH 08/12] Update advanced setup docs to include some
 documentation for new URLSessionClient class.

---
 docs/source/initialization.md | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/docs/source/initialization.md b/docs/source/initialization.md
index 7823049514..ec06b2cbc8 100644
--- a/docs/source/initialization.md
+++ b/docs/source/initialization.md
@@ -35,11 +35,21 @@ The available implementations are:
 
 The initializer for `HTTPNetworkTransport` has several properties which can allow you to get better information and finer-grained control of your HTTP requests and responses:
 
-- `session` allows you to pass in a custom `URLSession` to set up anything which needs to be done for every single request without alteration. This defaults to `URLSession.shared`. 
+- `client` allows you to pass in a [subclass of `URLSessionClient`](#the-urlsessionclient-class) to handle managing a background-compatible URL session, and set up anything which needs to be done for every single request without alteration. 
 - `sendOperationIdentifiers` allows you send operation identifiers along with your requests. **NOTE:** To send operation identifiers, Apollo types must be generated with `operationIdentifier`s or sending data will crash. Due to this restriction, this option defaults to `false`.
 - `useGETForQueries` sends all requests of `query` type using `GET` instead of `POST`. This defaults to `false` to preserve existing behavior in older versions of the client. 
 - `delegate` Can conform to one or many of several sub-protocols for `HTTPNetworkTransportDelegate`, detailed below.
 
+### The URLSessionClient class
+
+Since `URLSession` only supports use in the background using the delegate-based API, we have created our own `URLSessionClient` which handles the basics of setup for that. 
+
+One thing to be aware of: Because setting up a delegate is only possible in the initializer for `URLSession`, you can only pass in a `URLSessionConfiguration`, **not** an existing `URLSession`, to this class's initializer. 
+
+By default, instances of `URLSessionClient` use `URLSessionConfiguration.default` to set up their URL session, and instances of `HTTPNetworkTransport` use the default initializer for `URLSessionClient`.
+
+The `URLSessionClient` class and most of its methods are `open` so you can subclass it if you need to override any of the delegate methods for the `URLSession` delegates we're using or you need to handle additional delegate scenarios.  
+
 ### Using `HTTPNetworkTransportDelegate`
 
 This delegate includes several sub-protocols so that a single parameter can be passed no matter how many sub-protocols it conforms to. 

From 89da035e95ed8db4e12ed0b69d22770e32008ee9 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 17 Apr 2020 17:16:21 -0500
Subject: [PATCH 09/12] test two different types of cancellation

---
 Tests/ApolloTests/URLSessionClientTests.swift | 28 +++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/Tests/ApolloTests/URLSessionClientTests.swift b/Tests/ApolloTests/URLSessionClientTests.swift
index fe1091a1f8..3836ccc416 100644
--- a/Tests/ApolloTests/URLSessionClientTests.swift
+++ b/Tests/ApolloTests/URLSessionClientTests.swift
@@ -116,10 +116,34 @@ class URLSessionClientLiveTests: XCTestCase {
     self.wait(for: [expectation], timeout: 10)
   }
   
-  func testCancelling() throws {
+  func testCancellingTaskDirectlyCallsCompletionWithError() throws {
     let request = self.request(for: .bytes(count: 102400)) // 102400 is max from HTTPBin
     
-    let expectation = self.expectation(description: "Cancelled task completed anyway")
+    let expectation = self.expectation(description: "Cancelled task completed")
+    let task = self.client.sendRequest(request) { result in
+      defer {
+        expectation.fulfill()
+      }
+      
+      switch result {
+      case .failure(let error):
+        let nsError = error as NSError
+        XCTAssertEqual(nsError.domain, NSURLErrorDomain)
+        XCTAssertEqual(nsError.code, NSURLErrorCancelled)
+      case .success:
+        XCTFail("Task succeeded when it should have been cancelled!")
+      }
+    }
+    
+    task.cancel()
+    
+    self.wait(for: [expectation], timeout: 10)
+  }
+  
+  func testCancellingTaskThroughClientDoesNotCallCompletion() throws {
+    let request = self.request(for: .bytes(count: 102400)) // 102400 is max from HTTPBin
+    
+    let expectation = self.expectation(description: "Cancelled task completed")
     expectation.isInverted = true
     let task = self.client.sendRequest(request) { result in
       // This shouldn't get hit since we cancel the task immediately

From 97497c54c1831cfc300d2b594a71e522c2b01750 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Sun, 19 Apr 2020 14:19:47 -0500
Subject: [PATCH 10/12] add missing @available warnings for watchOS and tvOS

---
 Sources/Apollo/URLSessionClient.swift | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Sources/Apollo/URLSessionClient.swift b/Sources/Apollo/URLSessionClient.swift
index b1ef7b32a9..b290e381ab 100644
--- a/Sources/Apollo/URLSessionClient.swift
+++ b/Sources/Apollo/URLSessionClient.swift
@@ -102,7 +102,7 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     self.clearAllTasks()
   }
   
-  @available(OSX 10.12, iOS 10.0, *)
+  @available(OSX 10.12, iOS 10.0, tvOS 10.0, *)
   open func urlSession(_ session: URLSession,
                        task: URLSessionTask,
                        didFinishCollecting metrics: URLSessionTaskMetrics) {
@@ -187,7 +187,7 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     // No default implementation
   }
   
-  @available(iOS 11.0, OSXApplicationExtension 10.13, OSX 10.13, *)
+  @available(iOS 11.0, OSXApplicationExtension 10.13, OSX 10.13, tvOS 11.0, watchOS 4.0, *)
   open func urlSession(_ session: URLSession,
                        task: URLSessionTask,
                        willBeginDelayedRequest request: URLRequest,

From 4527d0b72d76567d571f5720baf1c1b938865d7f Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Sun, 19 Apr 2020 16:19:32 -0500
Subject: [PATCH 11/12] Add note about delegate methods to the class
 documentation of URLSessionClient

---
 Sources/Apollo/URLSessionClient.swift | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/Sources/Apollo/URLSessionClient.swift b/Sources/Apollo/URLSessionClient.swift
index b290e381ab..4de3e57030 100644
--- a/Sources/Apollo/URLSessionClient.swift
+++ b/Sources/Apollo/URLSessionClient.swift
@@ -2,6 +2,12 @@ import Foundation
 
 /// A class to handle URL Session calls that will support background execution,
 /// but still (mostly) use callbacks for its primary method of communication.
+///
+/// **NOTE:** Delegate methods implemented here are not documented inline because
+/// Apple has their own documentation for them. Please consult Apple's
+/// documentation for how the delegate methods work and what needs to be overridden
+/// and handled within your app, particularly in regards to what needs to be called
+/// when for background sessions.
 open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
   
   public enum URLSessionClientError: Error {

From d68300e6e5f5ffb19503e12e6446167473372047 Mon Sep 17 00:00:00 2001
From: Ellen Shapiro <designatednerd@gmail.com>
Date: Fri, 24 Apr 2020 18:21:23 -0500
Subject: [PATCH 12/12] add context to error being returned when something goes
 wrong at the network level

---
 Sources/Apollo/URLSessionClient.swift         | 17 +++++++++--------
 Tests/ApolloTests/URLSessionClientTests.swift | 13 ++++++++++---
 2 files changed, 19 insertions(+), 11 deletions(-)

diff --git a/Sources/Apollo/URLSessionClient.swift b/Sources/Apollo/URLSessionClient.swift
index 4de3e57030..471282e91f 100644
--- a/Sources/Apollo/URLSessionClient.swift
+++ b/Sources/Apollo/URLSessionClient.swift
@@ -14,6 +14,7 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     case noHTTPResponse(request: URLRequest?)
     case sessionBecameInvalidWithoutUnderlyingError
     case dataForRequestNotFound(request: URLRequest?)
+    case networkError(data: Data, response: HTTPURLResponse?, underlying: Error)
   }
   
   /// A completion block to be called when the raw task has completed, with the raw information from the session
@@ -160,16 +161,16 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
     if let rawCompletion = self.rawCompletions.removeValue(forKey: taskIdentifier) {
       rawCompletion(data, response, error)
     }
-      
+    
+    guard let finalData = data else {
+      // Data is immediately created for a task on creation, so if it's not there, something's gone wrong.
+      completion(.failure(URLSessionClientError.dataForRequestNotFound(request: task.originalRequest)))
+      return
+    }
+    
     if let finalError = error {
-      completion(.failure(finalError))
+      completion(.failure(URLSessionClientError.networkError(data: finalData, response: response, underlying: finalError)))
     } else {
-      guard let finalData = data else {
-        // Data is immediately created for a task on creation, so if it's not there, something's gone wrong.
-        completion(.failure(URLSessionClientError.dataForRequestNotFound(request: task.originalRequest)))
-        return
-      }
-      
       guard let finalResponse = response else {
         completion(.failure(URLSessionClientError.noHTTPResponse(request: task.originalRequest)))
         return
diff --git a/Tests/ApolloTests/URLSessionClientTests.swift b/Tests/ApolloTests/URLSessionClientTests.swift
index 3836ccc416..1602c1ba38 100644
--- a/Tests/ApolloTests/URLSessionClientTests.swift
+++ b/Tests/ApolloTests/URLSessionClientTests.swift
@@ -127,9 +127,16 @@ class URLSessionClientLiveTests: XCTestCase {
       
       switch result {
       case .failure(let error):
-        let nsError = error as NSError
-        XCTAssertEqual(nsError.domain, NSURLErrorDomain)
-        XCTAssertEqual(nsError.code, NSURLErrorCancelled)
+        switch error {
+        case URLSessionClient.URLSessionClientError.networkError(let data, let httpResponse, let underlying):
+          XCTAssertTrue(data.isEmpty)
+          XCTAssertNil(httpResponse)          
+          let nsError = underlying as NSError
+          XCTAssertEqual(nsError.domain, NSURLErrorDomain)
+          XCTAssertEqual(nsError.code, NSURLErrorCancelled)
+        default:
+          XCTFail("Unexpected error: \(error)")
+        }
       case .success:
         XCTFail("Task succeeded when it should have been cancelled!")
       }