Browse Source

Fix Validator Sendability (#3920)

### Issue Link :link:
Fixes #3919

### Goals :soccer:
This PR makes the validation system `Sendable`, fixing a runtime crash
when the consuming app is built in Swift 6 mode.

### Implementation Details :construction:
Adds `@Sendable` to the validation closures, and ensures the returned
`Error` is actually `Error & Sendable`.

### Testing Details :mag:
No additional tests.
Jon Shier 1 year ago
parent
commit
93a3bb5dcc

+ 65 - 32
.github/workflows/ci.yml

@@ -31,6 +31,11 @@ jobs:
       fail-fast: false
       matrix:
         include:
+          - xcode: "Xcode_16.1"
+            runsOn: firebreak
+            name: "macOS 14, Xcode 16.1, Swift 6.0.2"
+            testPlan: "macOS"
+            outputFilter: xcbeautify --renderer github-actions
           - xcode: "Xcode_16.0"
             runsOn: firebreak
             name: "macOS 14, Xcode 16.0, Swift 6.0"
@@ -74,6 +79,9 @@ jobs:
       fail-fast: false
       matrix:
         include:
+          - xcode: "Xcode_16.1"
+            name: "Catalyst 16.1"
+            runsOn: firebreak
           - xcode: "Xcode_16.0"
             name: "Catalyst 16.0"
             runsOn: firebreak
@@ -105,6 +113,11 @@ jobs:
       fail-fast: false
       matrix:
         include:
+          - destination: "OS=18.1,name=iPhone 16 Pro"
+            name: "iOS 18.1"
+            testPlan: "iOS"
+            xcode: "Xcode_16.1"
+            runsOn: firebreak
           - destination: "OS=18.0,name=iPhone 16 Pro"
             name: "iOS 18.0"
             testPlan: "iOS"
@@ -141,6 +154,11 @@ jobs:
       fail-fast: false
       matrix:
         include:
+          - destination: "OS=18.1,name=Apple TV"
+            name: "tvOS 18.1"
+            testPlan: "tvOS"
+            xcode: "Xcode_16.1"
+            runsOn: firebreak
           - destination: "OS=18.0,name=Apple TV"
             name: "tvOS 18.0"
             testPlan: "tvOS"
@@ -177,6 +195,12 @@ jobs:
       fail-fast: false
       matrix:
         include:
+          - destination: "OS=2.1,name=Apple Vision Pro"
+            name: "visionOS 2.1"
+            testPlan: "visionOS"
+            scheme: "Alamofire visionOS"
+            xcode: "Xcode_16.1"
+            runsOn: firebreak
           - destination: "OS=2.0,name=Apple Vision Pro"
             name: "visionOS 2.0"
             testPlan: "visionOS"
@@ -211,6 +235,11 @@ jobs:
       fail-fast: false
       matrix:
         include:
+          - destination: "OS=11.1,name=Apple Watch Series 10 (46mm)"
+            name: "watchOS 11.1"
+            testPlan: "watchOS"
+            xcode: "Xcode_16.1"
+            runsOn: firebreak
           - destination: "OS=11.0,name=Apple Watch Series 10 (46mm)"
             name: "watchOS 11.0"
             testPlan: "watchOS"
@@ -247,6 +276,10 @@ jobs:
       fail-fast: false
       matrix:
         include:
+          - xcode: "Xcode_16.1"
+            runsOn: firebreak
+            name: "macOS 14, SPM 6.0.2 Test"
+            outputFilter: xcbeautify --renderer github-actions
           - xcode: "Xcode_16.0"
             runsOn: firebreak
             name: "macOS 14, SPM 6.0 Test"
@@ -273,38 +306,38 @@ jobs:
         run: brew install alamofire/alamofire/firewalk || brew upgrade alamofire/alamofire/firewalk xcbeautify && firewalk &
       - name: Test SPM
         run: set -o pipefail && swift test -c debug 2>&1 | ${{ matrix.outputFilter }}
-  Linux:
-    name: Linux
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        include:
-          - image: swift:6.0-focal
-          - image: swift:6.0-jammy
-          - image: swift:6.0-rhel-ubi9
-          - image: swiftlang/swift:nightly-focal
-          - image: swiftlang/swift:nightly-jammy
-    container:
-      image: ${{ matrix.image }}
-    timeout-minutes: 10
-    steps:
-      - uses: actions/checkout@v4
-      - name: ${{ matrix.image }}
-        run: swift build --build-tests -c debug
-  Android:
-    name: Android
-    strategy:
-      fail-fast: false
-    runs-on: macos-13
-    steps:
-      - name: "Checkout"
-        uses: actions/checkout@v4
-      - name: "Build for Android"
-        uses: skiptools/swift-android-action@v1
-        with:
-          swift-build-flags: "--build-tests -c debug"
-          run-tests: false
+  # Linux:
+  #   name: Linux
+  #   runs-on: ubuntu-latest
+  #   strategy:
+  #     fail-fast: false
+  #     matrix:
+  #       include:
+  #         - image: swift:6.0-focal
+  #         - image: swift:6.0-jammy
+  #         - image: swift:6.0-rhel-ubi9
+  #         - image: swiftlang/swift:nightly-focal
+  #         - image: swiftlang/swift:nightly-jammy
+  #   container:
+  #     image: ${{ matrix.image }}
+  #   timeout-minutes: 10
+  #   steps:
+  #     - uses: actions/checkout@v4
+  #     - name: ${{ matrix.image }}
+  #       run: swift build --build-tests -c debug
+  # Android:
+  #   name: Android
+  #   strategy:
+  #     fail-fast: false
+  #   runs-on: macos-13
+  #   steps:
+  #     - name: "Checkout"
+  #       uses: actions/checkout@v4
+  #     - name: "Build for Android"
+  #       uses: skiptools/swift-android-action@v1
+  #       with:
+  #         swift-build-flags: "--build-tests -c debug"
+  #         run-tests: false
   Windows:
     name: ${{ matrix.name }}
     runs-on: windows-latest

+ 1 - 1
Source/Core/DataRequest.swift

@@ -143,7 +143,7 @@ public class DataRequest: Request, @unchecked Sendable {
     @preconcurrency
     @discardableResult
     public func validate(_ validation: @escaping Validation) -> Self {
-        let validator: () -> Void = { [unowned self] in
+        let validator: @Sendable () -> Void = { [unowned self] in
             guard error == nil, let response else { return }
 
             let result = validation(request, response, data)

+ 1 - 1
Source/Core/DataStreamRequest.swift

@@ -199,7 +199,7 @@ public final class DataStreamRequest: Request, @unchecked Sendable {
     /// - Returns:              The `DataStreamRequest`.
     @discardableResult
     public func validate(_ validation: @escaping Validation) -> Self {
-        let validator: () -> Void = { [unowned self] in
+        let validator: @Sendable () -> Void = { [unowned self] in
             guard error == nil, let response else { return }
 
             let result = validation(request, response)

+ 1 - 1
Source/Core/DownloadRequest.swift

@@ -303,7 +303,7 @@ public final class DownloadRequest: Request, @unchecked Sendable {
     /// - Returns:              The instance.
     @discardableResult
     public func validate(_ validation: @escaping Validation) -> Self {
-        let validator: () -> Void = { [unowned self] in
+        let validator: @Sendable () -> Void = { [unowned self] in
             guard error == nil, let response else { return }
 
             let result = validation(request, response, fileURL)

+ 1 - 1
Source/Core/Request.swift

@@ -188,7 +188,7 @@ public class Request: @unchecked Sendable {
     // MARK: Validators
 
     /// `Validator` callback closures that store the validation calls enqueued.
-    let validators = Protected<[() -> Void]>([])
+    let validators = Protected<[@Sendable () -> Void]>([])
 
     // MARK: URLRequests
 

+ 21 - 15
Source/Features/Validation.swift

@@ -30,7 +30,7 @@ extension Request {
     fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason
 
     /// Used to represent whether a validation succeeded or failed.
-    public typealias ValidationResult = Result<Void, any Error>
+    public typealias ValidationResult = Result<Void, any(Error & Sendable)>
 
     fileprivate struct MIMEType {
         let type: String
@@ -146,7 +146,7 @@ extension Request {
 extension DataRequest {
     /// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the
     /// request was valid.
-    public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult
+    public typealias Validation = @Sendable (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult
 
     /// Validates that the response has a status code in the specified sequence.
     ///
@@ -155,8 +155,9 @@ extension DataRequest {
     /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes.
     ///
     /// - Returns:                         The instance.
+    @preconcurrency
     @discardableResult
-    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
+    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int, S: Sendable {
         validate { [unowned self] _, response, _ in
             self.validate(statusCode: acceptableStatusCodes, response: response)
         }
@@ -169,8 +170,9 @@ extension DataRequest {
     /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
     ///
     /// - returns: The request.
+    @preconcurrency
     @discardableResult
-    public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String {
+    public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @Sendable @autoclosure () -> S) -> Self where S.Iterator.Element == String, S: Sendable {
         validate { [unowned self] _, response, data in
             self.validate(contentType: acceptableContentTypes(), response: response, isEmpty: (data == nil || data?.isEmpty == true))
         }
@@ -184,7 +186,7 @@ extension DataRequest {
     /// - returns: The request.
     @discardableResult
     public func validate() -> Self {
-        let contentTypes: () -> [String] = { [unowned self] in
+        let contentTypes: @Sendable () -> [String] = { [unowned self] in
             acceptableContentTypes
         }
         return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())
@@ -194,7 +196,7 @@ extension DataRequest {
 extension DataStreamRequest {
     /// A closure used to validate a request that takes a `URLRequest` and `HTTPURLResponse` and returns whether the
     /// request was valid.
-    public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse) -> ValidationResult
+    public typealias Validation = @Sendable (_ request: URLRequest?, _ response: HTTPURLResponse) -> ValidationResult
 
     /// Validates that the response has a status code in the specified sequence.
     ///
@@ -203,8 +205,9 @@ extension DataStreamRequest {
     /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes.
     ///
     /// - Returns:                         The instance.
+    @preconcurrency
     @discardableResult
-    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
+    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int, S: Sendable {
         validate { [unowned self] _, response in
             self.validate(statusCode: acceptableStatusCodes, response: response)
         }
@@ -217,8 +220,9 @@ extension DataStreamRequest {
     /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
     ///
     /// - returns: The request.
+    @preconcurrency
     @discardableResult
-    public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String {
+    public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @Sendable @autoclosure () -> S) -> Self where S.Iterator.Element == String, S: Sendable {
         validate { [unowned self] _, response in
             self.validate(contentType: acceptableContentTypes(), response: response)
         }
@@ -232,7 +236,7 @@ extension DataStreamRequest {
     /// - Returns: The instance.
     @discardableResult
     public func validate() -> Self {
-        let contentTypes: () -> [String] = { [unowned self] in
+        let contentTypes: @Sendable () -> [String] = { [unowned self] in
             acceptableContentTypes
         }
         return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())
@@ -244,9 +248,9 @@ extension DataStreamRequest {
 extension DownloadRequest {
     /// A closure used to validate a request that takes a URL request, a URL response, a temporary URL and a
     /// destination URL, and returns whether the request was valid.
-    public typealias Validation = (_ request: URLRequest?,
-                                   _ response: HTTPURLResponse,
-                                   _ fileURL: URL?)
+    public typealias Validation = @Sendable (_ request: URLRequest?,
+                                             _ response: HTTPURLResponse,
+                                             _ fileURL: URL?)
         -> ValidationResult
 
     /// Validates that the response has a status code in the specified sequence.
@@ -256,8 +260,9 @@ extension DownloadRequest {
     /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes.
     ///
     /// - Returns:                         The instance.
+    @preconcurrency
     @discardableResult
-    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
+    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int, S: Sendable {
         validate { [unowned self] _, response, _ in
             self.validate(statusCode: acceptableStatusCodes, response: response)
         }
@@ -270,8 +275,9 @@ extension DownloadRequest {
     /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes.
     ///
     /// - returns: The request.
+    @preconcurrency
     @discardableResult
-    public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String {
+    public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @Sendable @autoclosure () -> S) -> Self where S.Iterator.Element == String, S: Sendable {
         validate { [unowned self] _, response, fileURL in
             guard let fileURL else {
                 return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
@@ -294,7 +300,7 @@ extension DownloadRequest {
     /// - returns: The request.
     @discardableResult
     public func validate() -> Self {
-        let contentTypes = { [unowned self] in
+        let contentTypes: @Sendable () -> [String] = { [unowned self] in
             acceptableContentTypes
         }
         return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())