diff --git a/Modules/Sources/NetworkingCore/Network/AlamofireNetworkErrorHandler.swift b/Modules/Sources/NetworkingCore/Network/AlamofireNetworkErrorHandler.swift index 91d333a7be4..ba15c9e1804 100644 --- a/Modules/Sources/NetworkingCore/Network/AlamofireNetworkErrorHandler.swift +++ b/Modules/Sources/NetworkingCore/Network/AlamofireNetworkErrorHandler.swift @@ -85,15 +85,21 @@ final class AlamofireNetworkErrorHandler { originalRequest: URLRequestConvertible, failure: Error? ) { - let retriedRequestIndex = retriedJetpackRequests.firstIndex { retriedRequest in - let urlRequest = try? originalRequest.asURLRequest() - let retriedRequest = try? retriedRequest.request.asURLRequest() - return urlRequest == retriedRequest - } + let retriedRequest: RetriedJetpackRequest? = queue.sync(flags: .barrier) { [weak self] in + guard let self else { return nil } + + let retriedRequestIndex = _retriedJetpackRequests.firstIndex { retriedRequest in + let urlRequest = try? originalRequest.asURLRequest() + let retriedRequest = try? retriedRequest.request.asURLRequest() + return urlRequest == retriedRequest + } - guard let index = retriedRequestIndex else { return } + guard let index = retriedRequestIndex else { return nil } + + return _retriedJetpackRequests.remove(at: index) + } - let retriedRequest = retriedJetpackRequests.remove(at: index) + guard let retriedRequest else { return } if failure == nil { let siteID = retriedRequest.request.siteID diff --git a/Modules/Tests/NetworkingTests/Network/AlamofireNetworkErrorHandlerTests.swift b/Modules/Tests/NetworkingTests/Network/AlamofireNetworkErrorHandlerTests.swift index b7f834683b6..0f4cd702e80 100644 --- a/Modules/Tests/NetworkingTests/Network/AlamofireNetworkErrorHandlerTests.swift +++ b/Modules/Tests/NetworkingTests/Network/AlamofireNetworkErrorHandlerTests.swift @@ -560,6 +560,52 @@ final class AlamofireNetworkErrorHandlerTests: XCTestCase { // Then XCTAssertTrue(userDefaults.applicationPasswordUnsupportedList.keys.contains(String(siteID))) } + + func test_concurrent_flagSiteAsUnsupportedForAppPasswordIfNeeded_no_race_condition() { + // Given - test for the race condition fix where multiple threads + // try to remove the same item simultaneously + let expectation = XCTestExpectation(description: "All concurrent flag operations complete without crash") + let threadCount = 50 + let siteID: Int64 = 999 + let jetpackRequest = createJetpackRequest(siteID: siteID) + let restRequest = createRESTRequest() + let error = createNetworkError() + + expectation.expectedFulfillmentCount = threadCount + + // Add a single request to the retry list + _ = errorHandler.shouldRetryJetpackRequest( + originalRequest: jetpackRequest, + convertedRequest: restRequest, + failure: error + ) + + // When - multiple threads simultaneously try to flag and remove the SAME item + // This would cause a race condition in the old code where: + // 1. Thread A reads the array, finds index 0 + // 2. Thread B reads the array, finds index 0 + // 3. Thread A removes at index 0 (succeeds) + // 4. Thread B tries to remove at index 0 (crashes - array is now empty) + let group = DispatchGroup() + for _ in 0..