فهرست منبع

Allow Automatic Cancellation By Default (#3757)

### Issue Link :link:
#3727

### Goals :soccer:
This PR changes Alamofire's default behavior to better follow user
expectations around automatically cancellation when the enclosing async
context is cancelled.

This PR also updates some other async behaviors for more reliable tests
and behaviors.

### Implementation Details :construction:
Previously, Alamofire defaulted `shouldAutomaticallyCancel` to `true` in
order to follow what was then anticipated user expectations as well as
correct behavior for async APIs. However, many of the Swift Concurrency
cancellation behaviors that prompted this default turned out to be bugs.
Now that the correct behavior has been around for a while it's important
for Alamofire to fix its own behavior to match.

### Testing Details :mag:
Updated tests for new default and added tests when it's turned off.
Jon Shier 2 سال پیش
والد
کامیت
d732bc9edf
6فایلهای تغییر یافته به همراه160 افزوده شده و 70 حذف شده
  1. 2 3
      .github/workflows/ci.yml
  2. 43 43
      Source/Concurrency.swift
  3. 8 0
      Source/NetworkReachabilityManager.swift
  4. 3 8
      Source/Request.swift
  5. 103 15
      Tests/ConcurrencyTests.swift
  6. 1 1
      Tests/MultipartFormDataTests.swift

+ 2 - 3
.github/workflows/ci.yml

@@ -293,10 +293,9 @@ jobs:
           if not exist .build\x86_64-unknown-windows-msvc\debug\Alamofire.swiftmodule exit 1
           if not exist .build\x86_64-unknown-windows-msvc\debug\Alamofire.swiftmodule exit 1
   CodeQL:
   CodeQL:
     name: Analyze with CodeQL
     name: Analyze with CodeQL
-    runs-on: macOS-12
+    runs-on: macOS-13
     env:
     env:
-      DEVELOPER_DIR: "/Applications/Xcode_14.2.app/Contents/Developer"
-      CODEQL_ENABLE_EXPERIMENTAL_FEATURES_SWIFT: true
+      DEVELOPER_DIR: "/Applications/Xcode_14.3.1.app/Contents/Developer"
     timeout-minutes: 10
     timeout-minutes: 10
     steps:
     steps:
       - name: Clone
       - name: Clone

+ 43 - 43
Source/Concurrency.swift

@@ -37,7 +37,7 @@ extension Request {
     /// - Returns:                   The `StreamOf<Progress>`.
     /// - Returns:                   The `StreamOf<Progress>`.
     public func uploadProgress(bufferingPolicy: StreamOf<Progress>.BufferingPolicy = .unbounded) -> StreamOf<Progress> {
     public func uploadProgress(bufferingPolicy: StreamOf<Progress>.BufferingPolicy = .unbounded) -> StreamOf<Progress> {
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
-            uploadProgress(queue: .singleEventQueue) { progress in
+            uploadProgress(queue: underlyingQueue) { progress in
                 continuation.yield(progress)
                 continuation.yield(progress)
             }
             }
         }
         }
@@ -50,7 +50,7 @@ extension Request {
     /// - Returns:                   The `StreamOf<Progress>`.
     /// - Returns:                   The `StreamOf<Progress>`.
     public func downloadProgress(bufferingPolicy: StreamOf<Progress>.BufferingPolicy = .unbounded) -> StreamOf<Progress> {
     public func downloadProgress(bufferingPolicy: StreamOf<Progress>.BufferingPolicy = .unbounded) -> StreamOf<Progress> {
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
-            downloadProgress(queue: .singleEventQueue) { progress in
+            downloadProgress(queue: underlyingQueue) { progress in
                 continuation.yield(progress)
                 continuation.yield(progress)
             }
             }
         }
         }
@@ -63,7 +63,7 @@ extension Request {
     /// - Returns:                   The `StreamOf<URLRequest>`.
     /// - Returns:                   The `StreamOf<URLRequest>`.
     public func urlRequests(bufferingPolicy: StreamOf<URLRequest>.BufferingPolicy = .unbounded) -> StreamOf<URLRequest> {
     public func urlRequests(bufferingPolicy: StreamOf<URLRequest>.BufferingPolicy = .unbounded) -> StreamOf<URLRequest> {
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
-            onURLRequestCreation(on: .singleEventQueue) { request in
+            onURLRequestCreation(on: underlyingQueue) { request in
                 continuation.yield(request)
                 continuation.yield(request)
             }
             }
         }
         }
@@ -76,7 +76,7 @@ extension Request {
     /// - Returns:                   The `StreamOf<URLSessionTask>`.
     /// - Returns:                   The `StreamOf<URLSessionTask>`.
     public func urlSessionTasks(bufferingPolicy: StreamOf<URLSessionTask>.BufferingPolicy = .unbounded) -> StreamOf<URLSessionTask> {
     public func urlSessionTasks(bufferingPolicy: StreamOf<URLSessionTask>.BufferingPolicy = .unbounded) -> StreamOf<URLSessionTask> {
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
-            onURLSessionTaskCreation(on: .singleEventQueue) { task in
+            onURLSessionTaskCreation(on: underlyingQueue) { task in
                 continuation.yield(task)
                 continuation.yield(task)
             }
             }
         }
         }
@@ -89,7 +89,7 @@ extension Request {
     /// - Returns:                   The `StreamOf<String>`.
     /// - Returns:                   The `StreamOf<String>`.
     public func cURLDescriptions(bufferingPolicy: StreamOf<String>.BufferingPolicy = .unbounded) -> StreamOf<String> {
     public func cURLDescriptions(bufferingPolicy: StreamOf<String>.BufferingPolicy = .unbounded) -> StreamOf<String> {
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
         stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in
-            cURLDescription(on: .singleEventQueue) { description in
+            cURLDescription(on: underlyingQueue) { description in
                 continuation.yield(description)
                 continuation.yield(description)
             }
             }
         }
         }
@@ -173,13 +173,13 @@ extension DataRequest {
     /// - Parameters:
     /// - Parameters:
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before completion.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before completion.
     ///   - emptyResponseCodes:        HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
     ///   - emptyResponseCodes:        HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
     ///   - emptyRequestMethods:       `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
     ///   - emptyRequestMethods:       `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
     ///
     ///
     /// - Returns: The `DataTask`.
     /// - Returns: The `DataTask`.
-    public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = false,
+    public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
                                 dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
                                 dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
                                 emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
                                 emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
                                 emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataTask<Data> {
                                 emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataTask<Data> {
@@ -195,7 +195,7 @@ extension DataRequest {
     ///   - type:                      `Decodable` type to decode from response data.
     ///   - type:                      `Decodable` type to decode from response data.
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before calling the serializer.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before calling the serializer.
     ///                                `PassthroughPreprocessor()` by default.
     ///                                `PassthroughPreprocessor()` by default.
     ///   - decoder:                   `DataDecoder` to use to decode the response. `JSONDecoder()` by default.
     ///   - decoder:                   `DataDecoder` to use to decode the response. `JSONDecoder()` by default.
@@ -204,7 +204,7 @@ extension DataRequest {
     ///
     ///
     /// - Returns: The `DataTask`.
     /// - Returns: The `DataTask`.
     public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self,
     public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self,
-                                                       automaticallyCancelling shouldAutomaticallyCancel: Bool = false,
+                                                       automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
                                                        dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor,
                                                        dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor,
                                                        decoder: DataDecoder = JSONDecoder(),
                                                        decoder: DataDecoder = JSONDecoder(),
                                                        emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes,
                                                        emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes,
@@ -221,7 +221,7 @@ extension DataRequest {
     /// - Parameters:
     /// - Parameters:
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before calling the serializer.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before calling the serializer.
     ///                                `PassthroughPreprocessor()` by default.
     ///                                `PassthroughPreprocessor()` by default.
     ///   - encoding:                  `String.Encoding` to use during serialization. Defaults to `nil`, in which case
     ///   - encoding:                  `String.Encoding` to use during serialization. Defaults to `nil`, in which case
@@ -231,7 +231,7 @@ extension DataRequest {
     ///   - emptyRequestMethods:       `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
     ///   - emptyRequestMethods:       `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
     ///
     ///
     /// - Returns: The `DataTask`.
     /// - Returns: The `DataTask`.
-    public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = false,
+    public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
                                   dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
                                   dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
                                   encoding: String.Encoding? = nil,
                                   encoding: String.Encoding? = nil,
                                   emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
                                   emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
@@ -249,16 +249,16 @@ extension DataRequest {
     ///   - serializer:                `ResponseSerializer` responsible for serializing the request, response, and data.
     ///   - serializer:                `ResponseSerializer` responsible for serializing the request, response, and data.
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///
     ///
     /// - Returns: The `DataTask`.
     /// - Returns: The `DataTask`.
     public func serializingResponse<Serializer: ResponseSerializer>(using serializer: Serializer,
     public func serializingResponse<Serializer: ResponseSerializer>(using serializer: Serializer,
-                                                                    automaticallyCancelling shouldAutomaticallyCancel: Bool = false)
+                                                                    automaticallyCancelling shouldAutomaticallyCancel: Bool = true)
         -> DataTask<Serializer.SerializedObject> {
         -> DataTask<Serializer.SerializedObject> {
-        dataTask(automaticallyCancelling: shouldAutomaticallyCancel) {
-            self.response(queue: .singleEventQueue,
-                          responseSerializer: serializer,
-                          completionHandler: $0)
+        dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in
+            response(queue: underlyingQueue,
+                     responseSerializer: serializer,
+                     completionHandler: $0)
         }
         }
     }
     }
 
 
@@ -269,16 +269,16 @@ extension DataRequest {
     ///                                response, and data.
     ///                                response, and data.
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DataTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///
     ///
     /// - Returns: The `DataTask`.
     /// - Returns: The `DataTask`.
     public func serializingResponse<Serializer: DataResponseSerializerProtocol>(using serializer: Serializer,
     public func serializingResponse<Serializer: DataResponseSerializerProtocol>(using serializer: Serializer,
-                                                                                automaticallyCancelling shouldAutomaticallyCancel: Bool = false)
+                                                                                automaticallyCancelling shouldAutomaticallyCancel: Bool = true)
         -> DataTask<Serializer.SerializedObject> {
         -> DataTask<Serializer.SerializedObject> {
-        dataTask(automaticallyCancelling: shouldAutomaticallyCancel) {
-            self.response(queue: .singleEventQueue,
-                          responseSerializer: serializer,
-                          completionHandler: $0)
+        dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in
+            response(queue: underlyingQueue,
+                     responseSerializer: serializer,
+                     completionHandler: $0)
         }
         }
     }
     }
 
 
@@ -366,13 +366,13 @@ extension DownloadRequest {
     /// - Parameters:
     /// - Parameters:
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before completion.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before completion.
     ///   - emptyResponseCodes:        HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
     ///   - emptyResponseCodes:        HTTP response codes for which empty responses are allowed. `[204, 205]` by default.
     ///   - emptyRequestMethods:       `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
     ///   - emptyRequestMethods:       `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
     ///
     ///
     /// - Returns:                   The `DownloadTask`.
     /// - Returns:                   The `DownloadTask`.
-    public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = false,
+    public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
                                 dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
                                 dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
                                 emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
                                 emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
                                 emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask<Data> {
                                 emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask<Data> {
@@ -390,7 +390,7 @@ extension DownloadRequest {
     ///   - type:                      `Decodable` type to decode from response data.
     ///   - type:                      `Decodable` type to decode from response data.
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before calling the serializer.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before calling the serializer.
     ///                                `PassthroughPreprocessor()` by default.
     ///                                `PassthroughPreprocessor()` by default.
     ///   - decoder:                   `DataDecoder` to use to decode the response. `JSONDecoder()` by default.
     ///   - decoder:                   `DataDecoder` to use to decode the response. `JSONDecoder()` by default.
@@ -399,7 +399,7 @@ extension DownloadRequest {
     ///
     ///
     /// - Returns:                   The `DownloadTask`.
     /// - Returns:                   The `DownloadTask`.
     public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self,
     public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self,
-                                                       automaticallyCancelling shouldAutomaticallyCancel: Bool = false,
+                                                       automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
                                                        dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor,
                                                        dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor,
                                                        decoder: DataDecoder = JSONDecoder(),
                                                        decoder: DataDecoder = JSONDecoder(),
                                                        emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes,
                                                        emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes,
@@ -416,10 +416,10 @@ extension DownloadRequest {
     /// - Parameters:
     /// - Parameters:
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///
     ///
     /// - Returns: The `DownloadTask`.
     /// - Returns: The `DownloadTask`.
-    public func serializingDownloadedFileURL(automaticallyCancelling shouldAutomaticallyCancel: Bool = false) -> DownloadTask<URL> {
+    public func serializingDownloadedFileURL(automaticallyCancelling shouldAutomaticallyCancel: Bool = true) -> DownloadTask<URL> {
         serializingDownload(using: URLResponseSerializer(),
         serializingDownload(using: URLResponseSerializer(),
                             automaticallyCancelling: shouldAutomaticallyCancel)
                             automaticallyCancelling: shouldAutomaticallyCancel)
     }
     }
@@ -429,7 +429,7 @@ extension DownloadRequest {
     /// - Parameters:
     /// - Parameters:
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before calling the
     ///   - dataPreprocessor:          `DataPreprocessor` which processes the received `Data` before calling the
     ///                                serializer. `PassthroughPreprocessor()` by default.
     ///                                serializer. `PassthroughPreprocessor()` by default.
     ///   - encoding:                  `String.Encoding` to use during serialization. Defaults to `nil`, in which case
     ///   - encoding:                  `String.Encoding` to use during serialization. Defaults to `nil`, in which case
@@ -439,7 +439,7 @@ extension DownloadRequest {
     ///   - emptyRequestMethods:       `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
     ///   - emptyRequestMethods:       `HTTPMethod`s for which empty responses are always valid. `[.head]` by default.
     ///
     ///
     /// - Returns:                   The `DownloadTask`.
     /// - Returns:                   The `DownloadTask`.
-    public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = false,
+    public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = true,
                                   dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
                                   dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor,
                                   encoding: String.Encoding? = nil,
                                   encoding: String.Encoding? = nil,
                                   emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
                                   emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes,
@@ -457,16 +457,16 @@ extension DownloadRequest {
     ///   - serializer:                `ResponseSerializer` responsible for serializing the request, response, and data.
     ///   - serializer:                `ResponseSerializer` responsible for serializing the request, response, and data.
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///
     ///
     /// - Returns: The `DownloadTask`.
     /// - Returns: The `DownloadTask`.
     public func serializingDownload<Serializer: ResponseSerializer>(using serializer: Serializer,
     public func serializingDownload<Serializer: ResponseSerializer>(using serializer: Serializer,
-                                                                    automaticallyCancelling shouldAutomaticallyCancel: Bool = false)
+                                                                    automaticallyCancelling shouldAutomaticallyCancel: Bool = true)
         -> DownloadTask<Serializer.SerializedObject> {
         -> DownloadTask<Serializer.SerializedObject> {
-        downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) {
-            self.response(queue: .singleEventQueue,
-                          responseSerializer: serializer,
-                          completionHandler: $0)
+        downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in
+            response(queue: underlyingQueue,
+                     responseSerializer: serializer,
+                     completionHandler: $0)
         }
         }
     }
     }
 
 
@@ -478,16 +478,16 @@ extension DownloadRequest {
     ///                                response, and data.
     ///                                response, and data.
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///   - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
     ///                                enclosing async context is cancelled. Only applies to `DownloadTask`'s async
-    ///                                properties. `false` by default.
+    ///                                properties. `true` by default.
     ///
     ///
     /// - Returns: The `DownloadTask`.
     /// - Returns: The `DownloadTask`.
     public func serializingDownload<Serializer: DownloadResponseSerializerProtocol>(using serializer: Serializer,
     public func serializingDownload<Serializer: DownloadResponseSerializerProtocol>(using serializer: Serializer,
-                                                                                    automaticallyCancelling shouldAutomaticallyCancel: Bool = false)
+                                                                                    automaticallyCancelling shouldAutomaticallyCancel: Bool = true)
         -> DownloadTask<Serializer.SerializedObject> {
         -> DownloadTask<Serializer.SerializedObject> {
-        downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) {
-            self.response(queue: .singleEventQueue,
-                          responseSerializer: serializer,
-                          completionHandler: $0)
+        downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in
+            response(queue: underlyingQueue,
+                     responseSerializer: serializer,
+                     completionHandler: $0)
         }
         }
     }
     }
 
 

+ 8 - 0
Source/NetworkReachabilityManager.swift

@@ -266,11 +266,19 @@ extension SCNetworkReachabilityFlags {
     var canConnectWithoutUserInteraction: Bool { canConnectAutomatically && !contains(.interventionRequired) }
     var canConnectWithoutUserInteraction: Bool { canConnectAutomatically && !contains(.interventionRequired) }
     var isActuallyReachable: Bool { isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) }
     var isActuallyReachable: Bool { isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) }
     var isCellular: Bool {
     var isCellular: Bool {
+        #if swift(>=5.9)
+        #if os(iOS) || os(tvOS) || os(visionOS)
+        return contains(.isWWAN)
+        #else
+        return false
+        #endif
+        #else
         #if os(iOS) || os(tvOS)
         #if os(iOS) || os(tvOS)
         return contains(.isWWAN)
         return contains(.isWWAN)
         #else
         #else
         return false
         return false
         #endif
         #endif
+        #endif
     }
     }
 
 
     /// Human readable `String` for all states, to help with debugging.
     /// Human readable `String` for all states, to help with debugging.

+ 3 - 8
Source/Request.swift

@@ -863,13 +863,7 @@ public class Request {
     /// - Returns:           The instance.
     /// - Returns:           The instance.
     @discardableResult
     @discardableResult
     public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self {
     public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self {
-        $mutableState.write { mutableState in
-            if mutableState.requests.last != nil {
-                underlyingQueue.async { handler(self.cURLDescription()) }
-            } else {
-                mutableState.cURLHandler = (underlyingQueue, handler)
-            }
-        }
+        cURLDescription(on: underlyingQueue, calling: handler)
 
 
         return self
         return self
     }
     }
@@ -935,12 +929,13 @@ public class Request {
 
 
     /// Final cleanup step executed when the instance finishes response serialization.
     /// Final cleanup step executed when the instance finishes response serialization.
     func cleanup() {
     func cleanup() {
-        delegate?.cleanup(after: self)
         let handlers = $mutableState.finishHandlers
         let handlers = $mutableState.finishHandlers
         handlers.forEach { $0() }
         handlers.forEach { $0() }
         $mutableState.write { state in
         $mutableState.write { state in
             state.finishHandlers.removeAll()
             state.finishHandlers.removeAll()
         }
         }
+
+        delegate?.cleanup(after: self)
     }
     }
 }
 }
 
 

+ 103 - 15
Tests/ConcurrencyTests.swift

@@ -127,14 +127,14 @@ final class DataRequestConcurrencyTests: BaseTestCase {
         XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.")
         XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.")
     }
     }
 
 
-    func testThatDataTaskIsAutomaticallyCancelledInTaskWhenEnabled() async {
+    func testThatDataTaskIsAutomaticallyCancelledInTask() async {
         // Given
         // Given
         let session = stored(Session())
         let session = stored(Session())
         let request = session.request(.get)
         let request = session.request(.get)
 
 
         // When
         // When
         let task = Task {
         let task = Task {
-            await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).result
+            await request.serializingDecodable(TestResponse.self).result
         }
         }
 
 
         task.cancel()
         task.cancel()
@@ -146,7 +146,26 @@ final class DataRequestConcurrencyTests: BaseTestCase {
         XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.")
         XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.")
     }
     }
 
 
-    func testThatDataTaskIsAutomaticallyCancelledInTaskGroupWhenEnabled() async {
+    func testThatDataTaskIsNotAutomaticallyCancelledInTaskWhenDisabled() async {
+        // Given
+        let session = stored(Session())
+        let request = session.request(.get)
+
+        // When
+        let task = Task {
+            await request.serializingDecodable(TestResponse.self, automaticallyCancelling: false).result
+        }
+
+        task.cancel()
+        let result = await task.value
+
+        // Then
+        XCTAssertTrue(task.isCancelled, "Task should be cancelled.")
+        XCTAssertFalse(request.isCancelled, "Underlying DataRequest should not be cancelled.")
+        XCTAssertTrue(result.isSuccess, "DataRequest should succeed.")
+    }
+
+    func testThatDataTaskIsAutomaticallyCancelledInTaskGroup() async {
         // Given
         // Given
         let session = stored(Session())
         let session = stored(Session())
         let request = session.request(.get)
         let request = session.request(.get)
@@ -155,7 +174,7 @@ final class DataRequestConcurrencyTests: BaseTestCase {
         let task = Task {
         let task = Task {
             await withTaskGroup(of: Result<TestResponse, AFError>.self) { group -> Result<TestResponse, AFError> in
             await withTaskGroup(of: Result<TestResponse, AFError>.self) { group -> Result<TestResponse, AFError> in
                 group.addTask {
                 group.addTask {
-                    await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).result
+                    await request.serializingDecodable(TestResponse.self).result
                 }
                 }
 
 
                 return await group.first(where: { _ in true })!
                 return await group.first(where: { _ in true })!
@@ -170,6 +189,31 @@ final class DataRequestConcurrencyTests: BaseTestCase {
         XCTAssertTrue(task.isCancelled, "Task should be cancelled.")
         XCTAssertTrue(task.isCancelled, "Task should be cancelled.")
         XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.")
         XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.")
     }
     }
+
+    func testThatDataTaskIsNotAutomaticallyCancelledInTaskGroupWhenDisabled() async {
+        // Given
+        let session = stored(Session())
+        let request = session.request(.get)
+
+        // When
+        let task = Task {
+            await withTaskGroup(of: Result<TestResponse, AFError>.self) { group -> Result<TestResponse, AFError> in
+                group.addTask {
+                    await request.serializingDecodable(TestResponse.self, automaticallyCancelling: false).result
+                }
+
+                return await group.first(where: { _ in true })!
+            }
+        }
+
+        task.cancel()
+        let result = await task.value
+
+        // Then
+        XCTAssertTrue(task.isCancelled, "Task should be cancelled.")
+        XCTAssertFalse(request.isCancelled, "Underlying DataRequest should not be cancelled.")
+        XCTAssertTrue(result.isSuccess, "DataRequest should succeed.")
+    }
 }
 }
 
 
 @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
 @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@@ -282,14 +326,14 @@ final class DownloadConcurrencyTests: BaseTestCase {
         XCTAssertTrue(response.error?.isExplicitlyCancelledError == true)
         XCTAssertTrue(response.error?.isExplicitlyCancelledError == true)
     }
     }
 
 
-    func testThatDownloadTaskIsAutomaticallyCancelledInTaskWhenEnabled() async {
+    func testThatDownloadTaskIsAutomaticallyCancelledInTask() async {
         // Given
         // Given
         let session = stored(Session())
         let session = stored(Session())
         let request = session.download(.get)
         let request = session.download(.get)
 
 
         // When
         // When
         let task = Task {
         let task = Task {
-            await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).result
+            await request.serializingDecodable(TestResponse.self).result
         }
         }
 
 
         task.cancel()
         task.cancel()
@@ -301,7 +345,26 @@ final class DownloadConcurrencyTests: BaseTestCase {
         XCTAssertTrue(request.isCancelled, "Underlying DownloadRequest should be cancelled.")
         XCTAssertTrue(request.isCancelled, "Underlying DownloadRequest should be cancelled.")
     }
     }
 
 
-    func testThatDownloadTaskIsAutomaticallyCancelledInTaskGroupWhenEnabled() async {
+    func testThatDownloadTaskIsNotAutomaticallyCancelledInTaskWhenDisabled() async {
+        // Given
+        let session = stored(Session())
+        let request = session.download(.get)
+
+        // When
+        let task = Task {
+            await request.serializingDecodable(TestResponse.self, automaticallyCancelling: false).result
+        }
+
+        task.cancel()
+        let result = await task.value
+
+        // Then
+        XCTAssertTrue(task.isCancelled, "Task should be cancelled.")
+        XCTAssertFalse(request.isCancelled, "Underlying DownloadRequest should not be cancelled.")
+        XCTAssertTrue(result.isSuccess, "DownloadRequest should succeed.")
+    }
+
+    func testThatDownloadTaskIsAutomaticallyCancelledInTaskGroup() async {
         // Given
         // Given
         let session = stored(Session())
         let session = stored(Session())
         let request = session.download(.get)
         let request = session.download(.get)
@@ -310,7 +373,7 @@ final class DownloadConcurrencyTests: BaseTestCase {
         let task = Task {
         let task = Task {
             await withTaskGroup(of: Result<TestResponse, AFError>.self) { group -> Result<TestResponse, AFError> in
             await withTaskGroup(of: Result<TestResponse, AFError>.self) { group -> Result<TestResponse, AFError> in
                 group.addTask {
                 group.addTask {
-                    await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).result
+                    await request.serializingDecodable(TestResponse.self).result
                 }
                 }
 
 
                 return await group.first(where: { _ in true })!
                 return await group.first(where: { _ in true })!
@@ -325,6 +388,31 @@ final class DownloadConcurrencyTests: BaseTestCase {
         XCTAssertTrue(task.isCancelled, "Task should be cancelled.")
         XCTAssertTrue(task.isCancelled, "Task should be cancelled.")
         XCTAssertTrue(request.isCancelled, "Underlying DownloadRequest should be cancelled.")
         XCTAssertTrue(request.isCancelled, "Underlying DownloadRequest should be cancelled.")
     }
     }
+
+    func testThatDownloadTaskIsNotAutomaticallyCancelledInTaskGroupWhenDisabled() async {
+        // Given
+        let session = stored(Session())
+        let request = session.download(.get)
+
+        // When
+        let task = Task {
+            await withTaskGroup(of: Result<TestResponse, AFError>.self) { group -> Result<TestResponse, AFError> in
+                group.addTask {
+                    await request.serializingDecodable(TestResponse.self, automaticallyCancelling: false).result
+                }
+
+                return await group.first(where: { _ in true })!
+            }
+        }
+
+        task.cancel()
+        let result = await task.value
+
+        // Then
+        XCTAssertTrue(task.isCancelled, "Task should be cancelled.")
+        XCTAssertFalse(request.isCancelled, "Underlying DownloadRequest should not be cancelled.")
+        XCTAssertTrue(result.isSuccess, "DownloadRequest should succeed.")
+    }
 }
 }
 
 
 @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
 @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@@ -505,13 +593,13 @@ final class ClosureAPIConcurrencyTests: BaseTestCase {
         #endif
         #endif
 
 
         // Then
         // Then
-        XCTAssertTrue(values.uploadProgresses.isEmpty)
-        XCTAssertNotNil(values.downloadProgresses.last)
-        XCTAssertTrue(values.downloadProgresses.last?.isFinished == true)
-        XCTAssertNotNil(values.requests.last)
-        XCTAssertNotNil(values.tasks.last)
-        XCTAssertNotNil(values.descriptions.last)
-        XCTAssertTrue(values.response.result.isSuccess)
+        XCTAssertTrue(values.uploadProgresses.isEmpty, "uploadProgresses should be empty")
+        XCTAssertNotNil(values.downloadProgresses.last, "downloadProgresses should not be empty")
+        XCTAssertTrue(values.downloadProgresses.last?.isFinished == true, "last download progression should be finished")
+        XCTAssertNotNil(values.requests.last, "requests should not be empty")
+        XCTAssertNotNil(values.tasks.last, "tasks should not be empty")
+        XCTAssertNotNil(values.descriptions.last, "descriptions should not be empty")
+        XCTAssertTrue(values.response.result.isSuccess, "request should succeed")
     }
     }
 }
 }
 
 

+ 1 - 1
Tests/MultipartFormDataTests.swift

@@ -632,7 +632,7 @@ final class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
             expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary))
             expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary))
             expectedFileData.append(Data((
             expectedFileData.append(Data((
                 "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" +
                 "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" +
-                "Content-Type: image/png\(crlf)\(crlf)").utf8
+                    "Content-Type: image/png\(crlf)\(crlf)").utf8
             ))
             ))
             expectedFileData.append(unicornImageData.prefix(Int(expectedFileStreamUploadLength)))
             expectedFileData.append(unicornImageData.prefix(Int(expectedFileStreamUploadLength)))
             expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary))
             expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary))