Răsfoiți Sursa

More Atomic Locking (#3984)

### Issue Link :link:
Attempts to fix #3978 

### Goals :soccer:
This PR is a speculative attempt to fix the continuation resumption
crash seen in #3978. However, I was unable to reproduce that issues,
even with more complicated test cases and over 50 million test
iterations. So this PR makes a few parts of `Request`'s state management
more atomic, especially around response serializer handling. It also
removes `@dynamicMemberLookup` from `Protected`, which should encourage
more atomic usage.

### Implementation Details :construction:
This PR attempts to unify what were separate lock access in the same
flow into single accesses that accomplish the same thing. Behaviorally
should be the same but may prevent any intermediate accesses to state
that might cause this issue.

### Testing Details :mag:
No new test scenarios, as no reproduction could be found.
Jon Shier 1 lună în urmă
părinte
comite
9ec78dee16

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

@@ -473,17 +473,17 @@ jobs:
     runs-on: macOS-15
     env:
       DEVELOPER_DIR: "/Applications/Xcode_16.4.app/Contents/Developer"
-    timeout-minutes: 10
+    timeout-minutes: 20
     steps:
       - name: Clone
         uses: actions/checkout@v5
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@v3
+        uses: github/codeql-action/init@v4
         with:
           languages: swift
       - name: Build macOS
         run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire macOS" -destination "platform=macOS" clean build | xcpretty
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@v2
+        uses: github/codeql-action/analyze@v4
         with:
           category: "/language:swift"

+ 0 - 12
Alamofire.xcodeproj/project.pbxproj

@@ -668,14 +668,9 @@
 		312D1E0C1FC2551400E51FF1 /* AdvancedUsage.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = AdvancedUsage.md; path = Documentation/AdvancedUsage.md; sourceTree = "<group>"; };
 		312FC4FE2CB079E400E48EAB /* InternalHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalHelpers.swift; sourceTree = "<group>"; };
 		31425AC0241F098000EE3CCC /* InternalRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalRequestTests.swift; sourceTree = "<group>"; };
-		3145E0E227977AA300949557 /* iOS-NoTS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "iOS-NoTS.xctestplan"; sourceTree = "<group>"; };
-		3145E0E32797A8EF00949557 /* tvOS-NoTS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "tvOS-NoTS.xctestplan"; sourceTree = "<group>"; };
 		3145E0E42797A8EF00949557 /* tvOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = tvOS.xctestplan; sourceTree = "<group>"; };
-		3145E0E52797A8EF00949557 /* tvOS-Old.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "tvOS-Old.xctestplan"; sourceTree = "<group>"; };
 		3145E0E62797D91600949557 /* watchOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = watchOS.xctestplan; sourceTree = "<group>"; };
-		3145E0E72797D94200949557 /* watchOS-NoTS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "watchOS-NoTS.xctestplan"; sourceTree = "<group>"; };
 		3145E0E82797D9E700949557 /* macOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = macOS.xctestplan; sourceTree = "<group>"; };
-		3145E0E92797D9E900949557 /* macOS-NoTS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "macOS-NoTS.xctestplan"; sourceTree = "<group>"; };
 		314998E927A6560600ABB856 /* Request+AlamofireTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Request+AlamofireTests.swift"; sourceTree = "<group>"; };
 		31501E872196962A005829F2 /* ParameterEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParameterEncoderTests.swift; sourceTree = "<group>"; };
 		31577E0A2676E72D001C7532 /* FUNDING.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = FUNDING.yml; sourceTree = "<group>"; };
@@ -718,7 +713,6 @@
 		31E021522E5BCF1900EEB257 /* OfflineRetrierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineRetrierTests.swift; sourceTree = "<group>"; };
 		31ED52E61D73889D00199085 /* AFError+AlamofireTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AFError+AlamofireTests.swift"; sourceTree = "<group>"; };
 		31EF4BF5279646450048A19D /* iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOS.xctestplan; sourceTree = "<group>"; };
-		31EF4BF627964B520048A19D /* iOS-Old.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "iOS-Old.xctestplan"; sourceTree = "<group>"; };
 		31F1AA4123F75AEE00C2BB80 /* Alamofire 5.0 Migration Guide.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = "Alamofire 5.0 Migration Guide.md"; path = "Documentation/Alamofire 5.0 Migration Guide.md"; sourceTree = "<group>"; };
 		31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Alamofire.swift"; sourceTree = "<group>"; };
 		31F9683B20BB70290009606F /* NSLoggingEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLoggingEventMonitor.swift; sourceTree = "<group>"; };
@@ -1019,16 +1013,10 @@
 		31EF4BF4279646000048A19D /* Test Plans */ = {
 			isa = PBXGroup;
 			children = (
-				3145E0E227977AA300949557 /* iOS-NoTS.xctestplan */,
-				31EF4BF627964B520048A19D /* iOS-Old.xctestplan */,
 				31EF4BF5279646450048A19D /* iOS.xctestplan */,
-				3145E0E92797D9E900949557 /* macOS-NoTS.xctestplan */,
 				3145E0E82797D9E700949557 /* macOS.xctestplan */,
-				3145E0E32797A8EF00949557 /* tvOS-NoTS.xctestplan */,
-				3145E0E52797A8EF00949557 /* tvOS-Old.xctestplan */,
 				3145E0E42797A8EF00949557 /* tvOS.xctestplan */,
 				317339432A43BF9E00D4EA0A /* visionOS.xctestplan */,
-				3145E0E72797D94200949557 /* watchOS-NoTS.xctestplan */,
 				3145E0E62797D91600949557 /* watchOS.xctestplan */,
 			);
 			path = "Test Plans";

+ 4 - 2
Source/Core/DataRequest.swift

@@ -29,7 +29,7 @@ public class DataRequest: Request, @unchecked Sendable {
     /// `URLRequestConvertible` value used to create `URLRequest`s for this instance.
     public let convertible: any URLRequestConvertible
     /// `Data` read from the server so far.
-    public var data: Data? { dataMutableState.data }
+    public var data: Data? { dataMutableState.read(\.data) }
 
     private struct DataMutableState {
         var data: Data?
@@ -148,7 +148,9 @@ public class DataRequest: Request, @unchecked Sendable {
 
             let result = validation(request, response, data)
 
-            if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) }
+            if case let .failure(error) = result {
+                self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error)))
+            }
 
             eventMonitor?.request(self,
                                   didValidateRequest: request,

+ 5 - 5
Source/Core/DownloadRequest.swift

@@ -119,14 +119,14 @@ public final class DownloadRequest: Request, @unchecked Sendable {
     /// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel).
     public var resumeData: Data? {
         #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation.
-        return mutableDownloadState.resumeData ?? error?.downloadResumeData
+        return mutableDownloadState.read(\.resumeData) ?? error?.downloadResumeData
         #else
-        return mutableDownloadState.resumeData
+        return mutableDownloadState.read(\.resumeData)
         #endif
     }
 
     /// If the download is successful, the `URL` where the file was downloaded.
-    public var fileURL: URL? { mutableDownloadState.fileURL }
+    public var fileURL: URL? { mutableDownloadState.read(\.fileURL) }
 
     // MARK: Initial State
 
@@ -184,7 +184,7 @@ public final class DownloadRequest: Request, @unchecked Sendable {
         eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result)
 
         switch result {
-        case let .success(url): mutableDownloadState.fileURL = url
+        case let .success(url): mutableDownloadState.write { $0.fileURL = url }
         case let .failure(error): self.error = error
         }
     }
@@ -279,7 +279,7 @@ public final class DownloadRequest: Request, @unchecked Sendable {
                 // Resume to ensure metrics are gathered.
                 task.resume()
                 task.cancel { resumeData in
-                    self.mutableDownloadState.resumeData = resumeData
+                    self.mutableDownloadState.write { $0.resumeData = resumeData }
                     self.underlyingQueue.async { self.didCancelTask(task) }
                     completionHandler(resumeData)
                 }

+ 0 - 10
Source/Core/Protected.swift

@@ -81,7 +81,6 @@ extension NSLock: Lock {}
 #endif
 
 /// A thread-safe wrapper around a value.
-@dynamicMemberLookup
 final class Protected<Value> {
     #if canImport(Darwin)
     private let lock = UnfairLock()
@@ -122,15 +121,6 @@ final class Protected<Value> {
     func write(_ value: Value) {
         write { $0 = value }
     }
-
-    subscript<Property>(dynamicMember keyPath: WritableKeyPath<Value, Property>) -> Property {
-        get { lock.around { value[keyPath: keyPath] } }
-        set { lock.around { value[keyPath: keyPath] = newValue } }
-    }
-
-    subscript<Property>(dynamicMember keyPath: KeyPath<Value, Property>) -> Property {
-        lock.around { value[keyPath: keyPath] }
-    }
 }
 
 #if compiler(>=6)

+ 68 - 69
Source/Core/Request.swift

@@ -100,7 +100,7 @@ public class Request: @unchecked Sendable {
         var urlSessionTaskHandler: (queue: DispatchQueue, handler: @Sendable (URLSessionTask) -> Void)?
         /// Response serialization closures that handle response parsing.
         var responseSerializers: [@Sendable () -> Void] = []
-        /// Response serialization completion closures executed once all response serializers are complete.
+        /// Response serialization completion closures for successful serializers, executed once all response serializers are complete.
         var responseSerializerCompletions: [@Sendable () -> Void] = []
         /// Whether response serializer processing is finished.
         var responseSerializerProcessingFinished = false
@@ -128,7 +128,7 @@ public class Request: @unchecked Sendable {
     let mutableState = Protected(MutableState())
 
     /// `State` of the `Request`.
-    public var state: State { mutableState.state }
+    public var state: State { mutableState.read(\.state) }
     /// Returns whether `state` is `.initialized`.
     public var isInitialized: Bool { state == .initialized }
     /// Returns whether `state` is `.resumed`.
@@ -151,38 +151,38 @@ public class Request: @unchecked Sendable {
     public let downloadProgress = Progress(totalUnitCount: 0)
     /// `ProgressHandler` called when `uploadProgress` is updated, on the provided `DispatchQueue`.
     public internal(set) var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? {
-        get { mutableState.uploadProgressHandler }
-        set { mutableState.uploadProgressHandler = newValue }
+        get { mutableState.read(\.uploadProgressHandler) }
+        set { mutableState.write { $0.uploadProgressHandler = newValue } }
     }
 
     /// `ProgressHandler` called when `downloadProgress` is updated, on the provided `DispatchQueue`.
     public internal(set) var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? {
-        get { mutableState.downloadProgressHandler }
-        set { mutableState.downloadProgressHandler = newValue }
+        get { mutableState.read(\.downloadProgressHandler) }
+        set { mutableState.write { $0.downloadProgressHandler = newValue } }
     }
 
     // MARK: Redirect Handling
 
     /// `RedirectHandler` set on the instance.
     public internal(set) var redirectHandler: (any RedirectHandler)? {
-        get { mutableState.redirectHandler }
-        set { mutableState.redirectHandler = newValue }
+        get { mutableState.read(\.redirectHandler) }
+        set { mutableState.write { $0.redirectHandler = newValue } }
     }
 
     // MARK: Cached Response Handling
 
     /// `CachedResponseHandler` set on the instance.
     public internal(set) var cachedResponseHandler: (any CachedResponseHandler)? {
-        get { mutableState.cachedResponseHandler }
-        set { mutableState.cachedResponseHandler = newValue }
+        get { mutableState.read(\.cachedResponseHandler) }
+        set { mutableState.write { $0.cachedResponseHandler = newValue } }
     }
 
     // MARK: URLCredential
 
     /// `URLCredential` used for authentication challenges. Created by calling one of the `authenticate` methods.
     public internal(set) var credential: URLCredential? {
-        get { mutableState.credential }
-        set { mutableState.credential = newValue }
+        get { mutableState.read(\.credential) }
+        set { mutableState.write { $0.credential = newValue } }
     }
 
     // MARK: Validators
@@ -193,7 +193,7 @@ public class Request: @unchecked Sendable {
     // MARK: URLRequests
 
     /// All `URLRequest`s created on behalf of the `Request`, including original and adapted requests.
-    public var requests: [URLRequest] { mutableState.requests }
+    public var requests: [URLRequest] { mutableState.read(\.requests) }
     /// First `URLRequest` created on behalf of the `Request`. May not be the first one actually executed.
     public var firstRequest: URLRequest? { requests.first }
     /// Last `URLRequest` created on behalf of the `Request`.
@@ -214,7 +214,7 @@ public class Request: @unchecked Sendable {
     // MARK: Tasks
 
     /// All `URLSessionTask`s created on behalf of the `Request`.
-    public var tasks: [URLSessionTask] { mutableState.tasks }
+    public var tasks: [URLSessionTask] { mutableState.read(\.tasks) }
     /// First `URLSessionTask` created on behalf of the `Request`.
     public var firstTask: URLSessionTask? { tasks.first }
     /// Last `URLSessionTask` created on behalf of the `Request`.
@@ -225,7 +225,7 @@ public class Request: @unchecked Sendable {
     // MARK: Metrics
 
     /// All `URLSessionTaskMetrics` gathered on behalf of the `Request`. Should correspond to the `tasks` created.
-    public var allMetrics: [URLSessionTaskMetrics] { mutableState.metrics }
+    public var allMetrics: [URLSessionTaskMetrics] { mutableState.read(\.metrics) }
     /// First `URLSessionTaskMetrics` gathered on behalf of the `Request`.
     public var firstMetrics: URLSessionTaskMetrics? { allMetrics.first }
     /// Last `URLSessionTaskMetrics` gathered on behalf of the `Request`.
@@ -236,14 +236,14 @@ public class Request: @unchecked Sendable {
     // MARK: Retry Count
 
     /// Number of times the `Request` has been retried.
-    public var retryCount: Int { mutableState.retryCount }
+    public var retryCount: Int { mutableState.read(\.retryCount) }
 
     // MARK: Error
 
     /// `Error` returned from Alamofire internally, from the network request directly, or any validators executed.
     public internal(set) var error: AFError? {
-        get { mutableState.error }
-        set { mutableState.error = newValue }
+        get { mutableState.read(\.error) }
+        set { mutableState.write { $0.error = newValue } }
     }
 
     /// Default initializer for the `Request` superclass.
@@ -468,9 +468,9 @@ public class Request: @unchecked Sendable {
     func didCompleteTask(_ task: URLSessionTask, with error: AFError?) {
         dispatchPrecondition(condition: .onQueue(underlyingQueue))
 
-        self.error = self.error ?? error
+        mutableState.write { $0.error = $0.error ?? error }
 
-        let validators = validators.read { $0 }
+        let validators = validators.read(\.self)
         validators.forEach { $0() }
 
         eventMonitor?.request(self, didCompleteTask: task, with: error)
@@ -516,11 +516,17 @@ public class Request: @unchecked Sendable {
     func finish(error: AFError? = nil) {
         dispatchPrecondition(condition: .onQueue(underlyingQueue))
 
-        guard !mutableState.isFinishing else { return }
+        let shouldStartResponseSerializers = mutableState.write { mutableState in
+            guard !mutableState.isFinishing else { return false }
 
-        mutableState.isFinishing = true
+            mutableState.isFinishing = true
 
-        if let error { self.error = error }
+            if let error { mutableState.error = error }
+
+            return true
+        }
+
+        guard shouldStartResponseSerializers else { return }
 
         // Start response handlers
         processNextResponseSerializer()
@@ -541,6 +547,7 @@ public class Request: @unchecked Sendable {
                 mutableState.state = .resumed
             }
 
+            // If serializers have already been processed, execute the added serializer immediately.
             if mutableState.responseSerializerProcessingFinished {
                 underlyingQueue.async { self.processNextResponseSerializer() }
             }
@@ -551,32 +558,17 @@ public class Request: @unchecked Sendable {
         }
     }
 
-    /// Returns the next response serializer closure to execute if there's one left.
-    ///
-    /// - Returns: The next response serialization closure, if there is one.
-    func nextResponseSerializer() -> (@Sendable () -> Void)? {
-        var responseSerializer: (@Sendable () -> Void)?
-
-        mutableState.write { mutableState in
-            let responseSerializerIndex = mutableState.responseSerializerCompletions.count
-
-            if responseSerializerIndex < mutableState.responseSerializers.count {
-                responseSerializer = mutableState.responseSerializers[responseSerializerIndex]
-            }
-        }
-
-        return responseSerializer
-    }
-
     /// Processes the next response serializer and calls all completions if response serialization is complete.
     func processNextResponseSerializer() {
-        guard let responseSerializer = nextResponseSerializer() else {
-            // Execute all response serializer completions and clear them
-            var completions: [@Sendable () -> Void] = []
-
-            mutableState.write { mutableState in
-                completions = mutableState.responseSerializerCompletions
+        let executeOutside: () -> Void = mutableState.write { mutableState in
+            let responseSerializerIndex = mutableState.responseSerializerCompletions.count
+            let isAvailableSerializer = responseSerializerIndex < mutableState.responseSerializers.count
+            let responseSerializer = isAvailableSerializer ? mutableState.responseSerializers[responseSerializerIndex] : nil
 
+            if let responseSerializer {
+                return { self.serializationQueue.async { responseSerializer() } }
+            } else {
+                let completions = mutableState.responseSerializerCompletions
                 // Clear out all response serializers and response serializer completions in mutable state since the
                 // request is complete. It's important to do this prior to calling the completion closures in case
                 // the completions call back into the request triggering a re-processing of the response serializers.
@@ -590,17 +582,17 @@ public class Request: @unchecked Sendable {
 
                 mutableState.responseSerializerProcessingFinished = true
                 mutableState.isFinishing = false
-            }
-
-            completions.forEach { $0() }
 
-            // Cleanup the request
-            cleanup()
+                return {
+                    completions.forEach { $0() }
 
-            return
+                    // Cleanup the request outside the lock
+                    self.cleanup()
+                }
+            }
         }
 
-        serializationQueue.async { responseSerializer() }
+        executeOutside()
     }
 
     /// Notifies the `Request` that the response serializer is complete.
@@ -614,16 +606,15 @@ public class Request: @unchecked Sendable {
 
     /// Resets all task and response serializer related state for retry.
     func reset() {
-        error = nil
-
         uploadProgress.totalUnitCount = 0
         uploadProgress.completedUnitCount = 0
         downloadProgress.totalUnitCount = 0
         downloadProgress.completedUnitCount = 0
 
-        mutableState.write { state in
-            state.isFinishing = false
-            state.responseSerializerCompletions = []
+        mutableState.write { mutableState in
+            mutableState.error = nil
+            mutableState.isFinishing = false
+            mutableState.responseSerializerCompletions = []
         }
     }
 
@@ -757,7 +748,7 @@ public class Request: @unchecked Sendable {
     /// - Returns:              The instance.
     @discardableResult
     public func authenticate(with credential: URLCredential) -> Self {
-        mutableState.credential = credential
+        self.credential = credential
 
         return self
     }
@@ -774,7 +765,7 @@ public class Request: @unchecked Sendable {
     @preconcurrency
     @discardableResult
     public func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self {
-        mutableState.downloadProgressHandler = (handler: closure, queue: queue)
+        downloadProgressHandler = (handler: closure, queue: queue)
 
         return self
     }
@@ -791,7 +782,7 @@ public class Request: @unchecked Sendable {
     @preconcurrency
     @discardableResult
     public func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self {
-        mutableState.uploadProgressHandler = (handler: closure, queue: queue)
+        uploadProgressHandler = (handler: closure, queue: queue)
 
         return self
     }
@@ -846,7 +837,7 @@ public class Request: @unchecked Sendable {
     ///   - queue:   `DispatchQueue` on which `handler` will be called.
     ///   - handler: Closure to be called when the cURL description is available.
     ///
-    /// - Returns:           The instance.
+    /// - Returns:   The instance.
     @preconcurrency
     @discardableResult
     public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping @Sendable (String) -> Void) -> Self {
@@ -931,20 +922,28 @@ public class Request: @unchecked Sendable {
     ///
     /// - Parameter closure: Closure to be called when the request finishes.
     func onFinish(perform finishHandler: @escaping () -> Void) {
-        guard !isFinished else { finishHandler(); return }
+        let shouldImmediatelyExecute = mutableState.write { mutableState in
+            if mutableState.state == .finished {
+                return true
+            } else {
+                mutableState.finishHandlers.append(finishHandler)
+                return false
+            }
+        }
 
-        mutableState.write { state in
-            state.finishHandlers.append(finishHandler)
+        if shouldImmediatelyExecute {
+            finishHandler()
         }
     }
 
     /// Final cleanup step executed when the instance finishes response serialization.
     func cleanup() {
-        let handlers = mutableState.finishHandlers
-        handlers.forEach { $0() }
-        mutableState.write { state in
-            state.finishHandlers.removeAll()
+        let finishHandlers = mutableState.write { mutableState in
+            let handlers = mutableState.finishHandlers
+            mutableState.finishHandlers.removeAll()
+            return handlers
         }
+        finishHandlers.forEach { $0() }
 
         delegate?.cleanup(after: self)
     }

+ 2 - 2
Source/Features/AuthenticationInterceptor.swift

@@ -217,8 +217,8 @@ public final class AuthenticationInterceptor<AuthenticatorType>: RequestIntercep
 
     /// The `Credential` used to authenticate requests.
     public var credential: Credential? {
-        get { mutableState.credential }
-        set { mutableState.credential = newValue }
+        get { mutableState.read(\.credential) }
+        set { mutableState.write { $0.credential = newValue } }
     }
 
     let authenticator: AuthenticatorType

+ 1 - 1
Source/Features/MultipartUpload.swift

@@ -55,7 +55,7 @@ final class MultipartUpload: @unchecked Sendable { // Must be @unchecked due to
 
     func build() throws -> UploadRequest.Uploadable {
         let uploadable: UploadRequest.Uploadable
-        if multipartFormData.contentLength < encodingMemoryThreshold {
+        if multipartFormData.read(\.contentLength) < encodingMemoryThreshold {
             let data = try multipartFormData.read { try $0.encode() }
 
             uploadable = .data(data)

+ 5 - 3
Source/Features/OfflineRetrier.swift

@@ -22,6 +22,7 @@
 //  THE SOFTWARE.
 //
 
+#if canImport(Network)
 import Foundation
 import Network
 
@@ -65,7 +66,7 @@ public final class OfflineRetrier: RequestInterceptor, Sendable {
     ///   - monitor:        `NWPathMonitor()` to use to detect connectivity. A new instance is created each time a
     ///                     request fails and retry may be needed.
     ///   - maximumWait:    `DispatchTimeInterval` to wait for connectivity before
-    ///   - isOfflineError: Predicate closure used to determine whether a paricular `any Error` indicates connectivity
+    ///   - isOfflineError: Predicate closure used to determine whether a particular `any Error` indicates connectivity
     ///                     is offline. Returning `false` moves to the next retrier, if any.
     ///
     public init(monitor: @autoclosure @escaping () -> NWPathMonitor = NWPathMonitor(),
@@ -80,7 +81,7 @@ public final class OfflineRetrier: RequestInterceptor, Sendable {
     /// - Parameters:
     ///   - monitor:        `NWInterface.InterfaceType` used to configured the `NWPathMonitor` each time one is needed.
     ///   - maximumWait:    `DispatchTimeInterval` to wait for connectivity before
-    ///   - isOfflineError: Predicate closure used to determine whether a paricular `any Error` indicates connectivity
+    ///   - isOfflineError: Predicate closure used to determine whether a particular `any Error` indicates connectivity
     ///                     is offline. Returning `false` moves to the next retrier, if any.
     ///
     public convenience init(requiredInterfaceType: NWInterface.InterfaceType,
@@ -95,7 +96,7 @@ public final class OfflineRetrier: RequestInterceptor, Sendable {
     /// - Parameters:
     ///   - monitor:        `[NWInterface.InterfaceType]` used to configured the `NWPathMonitor` each time one is needed.
     ///   - maximumWait:    `DispatchTimeInterval` to wait for connectivity before
-    ///   - isOfflineError: Predicate closure used to determine whether a paricular `any Error` indicates connectivity
+    ///   - isOfflineError: Predicate closure used to determine whether a particular `any Error` indicates connectivity
     ///                     is offline. Returning `false` moves to the next retrier, if any.
     ///
     @available(macOS 11, iOS 14, tvOS 14, watchOS 7, visionOS 1, *)
@@ -278,3 +279,4 @@ extension PathMonitor {
         }
     }
 }
+#endif

+ 10 - 6
Tests/NSLoggingEventMonitor.swift

@@ -164,20 +164,24 @@ public final class NSLoggingEventMonitor: EventMonitor {
         NSLog("%@", "Request: \(request) didCancelTask: \(task)")
     }
 
-    public func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, any Error>) {
+    public func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {
         NSLog("%@", "Request: \(request), didParseResponse: \(response)")
     }
 
-    public func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, any Error>) {
+    public func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
         NSLog("%@", "Request: \(request), didParseResponse: \(response)")
     }
 
-    public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Data?, any Error>) {
-        NSLog("%@", "Request: \(request), didParseResponse: \(response)")
+    public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) {
+        NSLog("%@", "Download: \(request), didParseResponse: \(response)")
     }
 
-    public func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, any Error>) {
-        NSLog("%@", "Request: \(request), didParseResponse: \(response)")
+    public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Data?, AFError>) {
+        NSLog("%@", "Download: \(request), didParseResponse: \(response)")
+    }
+
+    public func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) {
+        NSLog("%@", "Download: \(request), didParseResponse: \(response)")
     }
 
     public func requestIsRetrying(_ request: Request) {

+ 2 - 0
Tests/OfflineRetrierTests.swift

@@ -1,3 +1,4 @@
+#if canImport(Networking)
 import Dispatch
 import Testing
 
@@ -226,3 +227,4 @@ struct OfflineRetrierTests {
         #expect(didStop.value == false)
     }
 }
+#endif

+ 2 - 19
Tests/ProtectedTests.swift

@@ -81,20 +81,6 @@ final class ProtectedWrapperTests: BaseTestCase {
         // Then
         XCTAssertNotEqual(value.read { $0 }, initialValue)
     }
-
-    func testThatDynamicMembersAreSetSafely() {
-        // Given
-        struct Mutable { var string = "value" }
-        let mutable = Protected<Mutable>(.init())
-
-        // When
-        DispatchQueue.concurrentPerform(iterations: 10_000) { i in
-            mutable.string = "\(i)"
-        }
-
-        // Then
-        XCTAssertNotEqual(mutable.string, "value")
-    }
 }
 
 final class ProtectedHighContentionTests: BaseTestCase {
@@ -228,9 +214,7 @@ final class ProtectedHighContentionTests: BaseTestCase {
 
         for _ in 1...totalOperations {
             queue1.async {
-                // Reads the total string count in the string array
-                // Using the wrapped value (no $) instead of the wrapper itself triggers the thread sanitizer.
-                let result = self.stringContainer.totalStrings
+                let result = self.stringContainer.read(\.totalStrings)
 
                 self.stringContainerRead.write {
                     $0.results1.append(result)
@@ -246,8 +230,7 @@ final class ProtectedHighContentionTests: BaseTestCase {
             }
 
             queue2.async {
-                // Reads the total string count in the string array
-                let result = self.stringContainer.read { $0.totalStrings }
+                let result = self.stringContainer.read(\.totalStrings)
 
                 self.stringContainerRead.write {
                     $0.results2.append(result)

+ 10 - 10
Tests/SessionTests.swift

@@ -105,11 +105,11 @@ final class SessionTestCase: BaseTestCase {
 
         private let mutableState: Protected<MutableState>
 
-        var adaptCalledCount: Int { mutableState.adaptCalledCount }
-        var adaptedCount: Int { mutableState.adaptedCount }
-        var retryCalledCount: Int { mutableState.retryCalledCount }
-        var retryCount: Int { mutableState.retryCount }
-        var retryErrors: [any Error] { mutableState.retryErrors }
+        var adaptCalledCount: Int { mutableState.read(\.adaptCalledCount) }
+        var adaptedCount: Int { mutableState.read(\.adaptedCount) }
+        var retryCalledCount: Int { mutableState.read(\.retryCalledCount) }
+        var retryCount: Int { mutableState.read(\.retryCount) }
+        var retryErrors: [any Error] { mutableState.read(\.retryErrors) }
 
         init(adaptedCount: Int = 0,
              throwsErrorOnSecondAdapt: Bool = false,
@@ -202,11 +202,11 @@ final class SessionTestCase: BaseTestCase {
 
         private let mutableState = Protected(MutableState())
 
-        var adaptCalledCount: Int { mutableState.adaptCalledCount }
-        var adaptedCount: Int { mutableState.adaptedCount }
-        var retryCalledCount: Int { mutableState.retryCalledCount }
-        var retryCount: Int { mutableState.retryCount }
-        var retryErrors: [any Error] { mutableState.retryErrors }
+        var adaptCalledCount: Int { mutableState.read(\.adaptCalledCount) }
+        var adaptedCount: Int { mutableState.read(\.adaptedCount) }
+        var retryCalledCount: Int { mutableState.read(\.retryCalledCount) }
+        var retryCount: Int { mutableState.read(\.retryCount) }
+        var retryErrors: [any Error] { mutableState.read(\.retryErrors) }
 
         func adapt(_ urlRequest: URLRequest,
                    using state: RequestAdapterState,

+ 0 - 31
Tests/Test Plans/iOS-NoTS.xctestplan

@@ -1,31 +0,0 @@
-{
-  "configurations" : [
-    {
-      "id" : "06BDBD92-3173-4395-90BF-851B80FF1162",
-      "name" : "Default",
-      "options" : {
-
-      }
-    }
-  ],
-  "defaultOptions" : {
-    "codeCoverage" : false
-  },
-  "testTargets" : [
-    {
-      "skippedTests" : [
-        "ClosureAPIConcurrencyTests",
-        "DataRequestConcurrencyTests",
-        "DataStreamConcurrencyTests",
-        "DownloadConcurrencyTests",
-        "WebSocketConcurrencyTests"
-      ],
-      "target" : {
-        "containerPath" : "container:Alamofire.xcodeproj",
-        "identifier" : "F8111E3D19A95C8B0040E7D1",
-        "name" : "Alamofire iOS Tests"
-      }
-    }
-  ],
-  "version" : 1
-}

+ 0 - 32
Tests/Test Plans/iOS-Old.xctestplan

@@ -1,32 +0,0 @@
-{
-  "configurations" : [
-    {
-      "id" : "06BDBD92-3173-4395-90BF-851B80FF1162",
-      "name" : "Default",
-      "options" : {
-
-      }
-    }
-  ],
-  "defaultOptions" : {
-    "codeCoverage" : false
-  },
-  "testTargets" : [
-    {
-      "skippedTests" : [
-        "CombineTestCase",
-        "DataRequestCombineTests",
-        "DataStreamRequestCombineTests",
-        "DownloadRequestCombineTests",
-        "WebSocketConcurrencyTests",
-        "WebSocketTests"
-      ],
-      "target" : {
-        "containerPath" : "container:Alamofire.xcodeproj",
-        "identifier" : "F8111E3D19A95C8B0040E7D1",
-        "name" : "Alamofire iOS Tests"
-      }
-    }
-  ],
-  "version" : 1
-}

+ 10 - 1
Tests/Test Plans/iOS.xctestplan

@@ -9,7 +9,16 @@
     }
   ],
   "defaultOptions" : {
-    "codeCoverage" : false
+    "codeCoverage" : false,
+    "mainThreadCheckerDetectionPolicy" : {
+      "enabled" : false
+    },
+    "runtimeIssueDetection" : {
+      "enabled" : false
+    },
+    "threadPerformanceCheckerRuntimeIssueDetection" : {
+      "enabled" : false
+    }
   },
   "testTargets" : [
     {

+ 0 - 31
Tests/Test Plans/macOS-NoTS.xctestplan

@@ -1,31 +0,0 @@
-{
-  "configurations" : [
-    {
-      "id" : "06BDBD92-3173-4395-90BF-851B80FF1162",
-      "name" : "Default",
-      "options" : {
-
-      }
-    }
-  ],
-  "defaultOptions" : {
-    "codeCoverage" : false
-  },
-  "testTargets" : [
-    {
-      "skippedTests" : [
-        "ClosureAPIConcurrencyTests",
-        "DataRequestConcurrencyTests",
-        "DataStreamConcurrencyTests",
-        "DownloadConcurrencyTests",
-        "WebSocketConcurrencyTests"
-      ],
-      "target" : {
-        "containerPath" : "container:Alamofire.xcodeproj",
-        "identifier" : "F829C6B11A7A94F100A2CD59",
-        "name" : "Alamofire macOS Tests"
-      }
-    }
-  ],
-  "version" : 1
-}

+ 10 - 1
Tests/Test Plans/macOS.xctestplan

@@ -9,7 +9,16 @@
     }
   ],
   "defaultOptions" : {
-    "codeCoverage" : false
+    "codeCoverage" : false,
+    "mainThreadCheckerDetectionPolicy" : {
+      "enabled" : false
+    },
+    "runtimeIssueDetection" : {
+      "enabled" : false
+    },
+    "threadPerformanceCheckerRuntimeIssueDetection" : {
+      "enabled" : false
+    }
   },
   "testTargets" : [
     {

+ 0 - 31
Tests/Test Plans/tvOS-NoTS.xctestplan

@@ -1,31 +0,0 @@
-{
-  "configurations" : [
-    {
-      "id" : "06BDBD92-3173-4395-90BF-851B80FF1162",
-      "name" : "Default",
-      "options" : {
-
-      }
-    }
-  ],
-  "defaultOptions" : {
-    "codeCoverage" : false
-  },
-  "testTargets" : [
-    {
-      "skippedTests" : [
-        "ClosureAPIConcurrencyTests",
-        "DataRequestConcurrencyTests",
-        "DataStreamConcurrencyTests",
-        "DownloadConcurrencyTests",
-        "WebSocketConcurrencyTests"
-      ],
-      "target" : {
-        "containerPath" : "container:Alamofire.xcodeproj",
-        "identifier" : "4CF626F71BA7CB3E0011A099",
-        "name" : "Alamofire tvOS Tests"
-      }
-    }
-  ],
-  "version" : 1
-}

+ 0 - 36
Tests/Test Plans/tvOS-Old.xctestplan

@@ -1,36 +0,0 @@
-{
-  "configurations" : [
-    {
-      "id" : "06BDBD92-3173-4395-90BF-851B80FF1162",
-      "name" : "Default",
-      "options" : {
-
-      }
-    }
-  ],
-  "defaultOptions" : {
-    "codeCoverage" : false
-  },
-  "testTargets" : [
-    {
-      "skippedTests" : [
-        "ClosureAPIConcurrencyTests",
-        "CombineTestCase",
-        "DataRequestCombineTests",
-        "DataRequestConcurrencyTests",
-        "DataStreamConcurrencyTests",
-        "DataStreamRequestCombineTests",
-        "DownloadConcurrencyTests",
-        "DownloadRequestCombineTests",
-        "WebSocketConcurrencyTests",
-        "WebSocketTests"
-      ],
-      "target" : {
-        "containerPath" : "container:Alamofire.xcodeproj",
-        "identifier" : "4CF626F71BA7CB3E0011A099",
-        "name" : "Alamofire tvOS Tests"
-      }
-    }
-  ],
-  "version" : 1
-}

+ 10 - 1
Tests/Test Plans/tvOS.xctestplan

@@ -9,7 +9,16 @@
     }
   ],
   "defaultOptions" : {
-    "codeCoverage" : false
+    "codeCoverage" : false,
+    "mainThreadCheckerDetectionPolicy" : {
+      "enabled" : false
+    },
+    "runtimeIssueDetection" : {
+      "enabled" : false
+    },
+    "threadPerformanceCheckerRuntimeIssueDetection" : {
+      "enabled" : false
+    }
   },
   "testTargets" : [
     {

+ 11 - 3
Tests/Test Plans/visionOS.xctestplan

@@ -2,14 +2,22 @@
   "configurations" : [
     {
       "id" : "D2E9E261-0428-48C6-A483-07B3473E499C",
-      "name" : "Configuration 1",
+      "name" : "Default",
       "options" : {
-
+        "threadSanitizerEnabled" : true
       }
     }
   ],
   "defaultOptions" : {
-    "threadSanitizerEnabled" : true
+    "mainThreadCheckerDetectionPolicy" : {
+      "enabled" : false
+    },
+    "runtimeIssueDetection" : {
+      "enabled" : false
+    },
+    "threadPerformanceCheckerRuntimeIssueDetection" : {
+      "enabled" : false
+    }
   },
   "testTargets" : [
     {

+ 0 - 31
Tests/Test Plans/watchOS-NoTS.xctestplan

@@ -1,31 +0,0 @@
-{
-  "configurations" : [
-    {
-      "id" : "13FE6F19-6B66-4BD2-862D-9F091CA8B792",
-      "name" : "Default",
-      "options" : {
-
-      }
-    }
-  ],
-  "defaultOptions" : {
-    "codeCoverage" : false
-  },
-  "testTargets" : [
-    {
-      "skippedTests" : [
-        "ClosureAPIConcurrencyTests",
-        "DataRequestConcurrencyTests",
-        "DataStreamConcurrencyTests",
-        "DownloadConcurrencyTests",
-        "WebSocketConcurrencyTests"
-      ],
-      "target" : {
-        "containerPath" : "container:Alamofire.xcodeproj",
-        "identifier" : "31293064263E17D600473CEA",
-        "name" : "Alamofire watchOS Tests"
-      }
-    }
-  ],
-  "version" : 1
-}

+ 9 - 1
Tests/Test Plans/watchOS.xctestplan

@@ -10,7 +10,15 @@
   ],
   "defaultOptions" : {
     "codeCoverage" : false,
-    "nsZombieEnabled" : true
+    "mainThreadCheckerDetectionPolicy" : {
+      "enabled" : false
+    },
+    "runtimeIssueDetection" : {
+      "enabled" : false
+    },
+    "threadPerformanceCheckerRuntimeIssueDetection" : {
+      "enabled" : false
+    }
   },
   "testTargets" : [
     {