diff --git a/Sources/FoundationNetworking/URLSession/FTP/FTPURLProtocol.swift b/Sources/FoundationNetworking/URLSession/FTP/FTPURLProtocol.swift index 8f87e884cf..f504952c07 100644 --- a/Sources/FoundationNetworking/URLSession/FTP/FTPURLProtocol.swift +++ b/Sources/FoundationNetworking/URLSession/FTP/FTPURLProtocol.swift @@ -109,6 +109,8 @@ internal extension _FTPURLProtocol { self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) case .dataCompletionHandler: break + case .dataCompletionHandlerWithTaskDelegate: + break case .downloadCompletionHandler: break } diff --git a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift index 9695be004e..2e63adde8f 100644 --- a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift +++ b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift @@ -525,6 +525,8 @@ internal class _HTTPURLProtocol: _NativeProtocol { } case .dataCompletionHandler: break + case .dataCompletionHandlerWithTaskDelegate: + break case .downloadCompletionHandler: break } diff --git a/Sources/FoundationNetworking/URLSession/NativeProtocol.swift b/Sources/FoundationNetworking/URLSession/NativeProtocol.swift index 53a195f5a8..1cdb725be9 100644 --- a/Sources/FoundationNetworking/URLSession/NativeProtocol.swift +++ b/Sources/FoundationNetworking/URLSession/NativeProtocol.swift @@ -338,7 +338,8 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { // Data will be forwarded to the delegate as we receive it, we don't // need to do anything about it. return .ignore - case .dataCompletionHandler: + case .dataCompletionHandler, + .dataCompletionHandlerWithTaskDelegate: // Data needs to be concatenated in-memory such that we can pass it // to the completion handler upon completion. return .inMemory(nil) diff --git a/Sources/FoundationNetworking/URLSession/TaskRegistry.swift b/Sources/FoundationNetworking/URLSession/TaskRegistry.swift index 9066a4a9cc..b6b2117f49 100644 --- a/Sources/FoundationNetworking/URLSession/TaskRegistry.swift +++ b/Sources/FoundationNetworking/URLSession/TaskRegistry.swift @@ -45,6 +45,8 @@ extension URLSession { case callDelegate /// Default action for all events, except for completion. case dataCompletionHandler(DataTaskCompletion) + /// Default action for all asynchronous events. + case dataCompletionHandlerWithTaskDelegate(DataTaskCompletion, URLSessionTaskDelegate?) /// Default action for all events, except for completion. case downloadCompletionHandler(DownloadTaskCompletion) } diff --git a/Sources/FoundationNetworking/URLSession/URLSession.swift b/Sources/FoundationNetworking/URLSession/URLSession.swift index 2dcb000a22..630063ebfd 100644 --- a/Sources/FoundationNetworking/URLSession/URLSession.swift +++ b/Sources/FoundationNetworking/URLSession/URLSession.swift @@ -442,6 +442,34 @@ open class URLSession : NSObject { open func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { return dataTask(with: _Request(url), behaviour: .dataCompletionHandler(completionHandler)) } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + open func data(from url: URL, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) { + var task: URLSessionTask? = nil + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + let completionHandler: URLSession._TaskRegistry.DataTaskCompletion = { data, response, error in + if let data, let response { + continuation.resume(returning: (data, response)) + } else if let error { + continuation.resume(throwing: error) + } else { + fatalError() + } + } + task = dataTask(with: _Request(url), behaviour: .dataCompletionHandlerWithTaskDelegate(completionHandler, delegate)) + + if Task.isCancelled { + continuation.resume(throwing: CancellationError()) + return + } + task?.resume() + } + } onCancel: { [weak task] in + task?.cancel() + } + } /* Creates an upload task with the given request. The body of the request will be created from the file referenced by fileURL */ open func uploadTask(with request: URLRequest, fromFile fileURL: URL) -> URLSessionUploadTask { @@ -648,6 +676,9 @@ internal extension URLSession { /// Default action for all events, except for completion. /// - SeeAlso: URLSession.TaskRegistry.Behaviour.dataCompletionHandler case dataCompletionHandler(URLSession._TaskRegistry.DataTaskCompletion) + /// Default action for all asynchronous events. + /// - SeeAlso: URLsession.TaskRegistry.Behaviour.dataCompletionHandlerWithTaskDelegate + case dataCompletionHandlerWithTaskDelegate(URLSession._TaskRegistry.DataTaskCompletion, URLSessionTaskDelegate) /// Default action for all events, except for completion. /// - SeeAlso: URLSession.TaskRegistry.Behaviour.downloadCompletionHandler case downloadCompletionHandler(URLSession._TaskRegistry.DownloadTaskCompletion) @@ -656,6 +687,11 @@ internal extension URLSession { func behaviour(for task: URLSessionTask) -> _TaskBehaviour { switch taskRegistry.behaviour(for: task) { case .dataCompletionHandler(let c): return .dataCompletionHandler(c) + case .dataCompletionHandlerWithTaskDelegate(let c, let d): + guard let d else { + return .dataCompletionHandler(c) + } + return .dataCompletionHandlerWithTaskDelegate(c, d) case .downloadCompletionHandler(let c): return .downloadCompletionHandler(c) case .callDelegate: guard let d = delegate as? URLSessionTaskDelegate else { diff --git a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift index 6a342c6ad2..3e7e22e54d 100644 --- a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift +++ b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift @@ -28,7 +28,7 @@ private class Bag { /// A cancelable object that refers to the lifetime /// of processing a given request. -open class URLSessionTask : NSObject, NSCopying { +open class URLSessionTask : NSObject, NSCopying, @unchecked Sendable { // These properties aren't heeded in swift-corelibs-foundation, but we may heed them in the future. They exist for source compatibility. open var countOfBytesClientExpectsToReceive: Int64 = NSURLSessionTransferSizeUnknown { @@ -1058,7 +1058,7 @@ extension _ProtocolClient : URLProtocolClient { webSocketDelegate.urlSession(session, webSocketTask: webSocketTask, didOpenWithProtocol: webSocketTask.protocolPicked) } } - case .noDelegate, .dataCompletionHandler, .downloadCompletionHandler: + case .noDelegate, .dataCompletionHandler, .dataCompletionHandlerWithTaskDelegate, .downloadCompletionHandler: break } } @@ -1157,7 +1157,8 @@ extension _ProtocolClient : URLProtocolClient { session.workQueue.async { session.taskRegistry.remove(task) } - case .dataCompletionHandler(let completion): + case .dataCompletionHandler(let completion), + .dataCompletionHandlerWithTaskDelegate(let completion, _): session.delegateQueue.addOperation { guard task.state != .completed else { return } completion(urlProtocol.properties[URLProtocol._PropertyKey.responseData] as? Data ?? Data(), task.response, nil) @@ -1297,7 +1298,8 @@ extension _ProtocolClient : URLProtocolClient { session.workQueue.async { session.taskRegistry.remove(task) } - case .dataCompletionHandler(let completion): + case .dataCompletionHandler(let completion), + .dataCompletionHandlerWithTaskDelegate(let completion, _): session.delegateQueue.addOperation { guard task.state != .completed else { return } completion(nil, nil, error) diff --git a/Tests/Foundation/Tests/TestURLSession.swift b/Tests/Foundation/Tests/TestURLSession.swift index 8c04855589..6cb5b1c3cd 100644 --- a/Tests/Foundation/Tests/TestURLSession.swift +++ b/Tests/Foundation/Tests/TestURLSession.swift @@ -92,6 +92,16 @@ class TestURLSession: LoopbackServerTest { task.resume() waitForExpectations(timeout: 12) } + + func test_dataFromURLDelegate() async throws { + guard #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) else { return } + let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/UK" + let (data, response) = try await URLSession.shared.data(from: URL(string: urlString)!, delegate: nil) + guard let httpResponse = response as? HTTPURLResponse else { return } + XCTAssertEqual(200, httpResponse.statusCode, "HTTP response code is not 200") + let expectedResult = String(data: data, encoding: .utf8) ?? "" + XCTAssertEqual("London", expectedResult, "Did not receive expected value") + } func test_dataTaskWithHttpInputStream() throws { let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/jsonBody" @@ -2100,6 +2110,7 @@ class TestURLSession: LoopbackServerTest { ] if #available(macOS 12.0, *) { retVal.append(contentsOf: [ + ("test_dataFromURLDelegate", asyncTest(test_dataFromURLDelegate)), ("test_webSocket", asyncTest(test_webSocket)), ("test_webSocketSpecificProtocol", asyncTest(test_webSocketSpecificProtocol)), ("test_webSocketAbruptClose", asyncTest(test_webSocketAbruptClose)),