Skip to content

Commit cc5c514

Browse files
authored
Fix race condition when flagging site as unsupported for application passwords (#16237)
2 parents 72fc7e9 + e1dcda9 commit cc5c514

File tree

2 files changed

+70
-11
lines changed

2 files changed

+70
-11
lines changed

Modules/Sources/NetworkingCore/Network/AlamofireNetworkErrorHandler.swift

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,23 @@ final class AlamofireNetworkErrorHandler {
8585
originalRequest: URLRequestConvertible,
8686
failure: Error?
8787
) {
88-
let retriedRequestIndex = retriedJetpackRequests.firstIndex { retriedRequest in
89-
let urlRequest = try? originalRequest.asURLRequest()
90-
let retriedRequest = try? retriedRequest.request.asURLRequest()
91-
return urlRequest == retriedRequest
92-
}
88+
let retriedRequest: RetriedJetpackRequest? = queue.sync(flags: .barrier) { [weak self] in
89+
guard let self else { return nil }
90+
guard let urlRequest = try? originalRequest.asURLRequest() else { return nil }
91+
let retriedRequestIndex = _retriedJetpackRequests.firstIndex { retriedRequest in
92+
guard let retriedURLRequest = try? retriedRequest.request.asURLRequest() else {
93+
return false
94+
}
95+
return urlRequest.url == retriedURLRequest.url &&
96+
urlRequest.httpMethod == retriedURLRequest.httpMethod
97+
}
9398

94-
guard let index = retriedRequestIndex else { return }
99+
guard let index = retriedRequestIndex else { return nil }
100+
101+
return _retriedJetpackRequests.remove(at: index)
102+
}
95103

96-
let retriedRequest = retriedJetpackRequests.remove(at: index)
104+
guard let retriedRequest else { return }
97105

98106
if failure == nil {
99107
let siteID = retriedRequest.request.siteID
@@ -141,10 +149,15 @@ final class AlamofireNetworkErrorHandler {
141149
}
142150

143151
func isRequestRetried(_ request: URLRequestConvertible) -> Bool {
144-
retriedJetpackRequests.contains { retriedRequest in
145-
let urlRequest = try? request.asURLRequest()
146-
let currentItem = try? retriedRequest.request.asURLRequest()
147-
return currentItem == urlRequest
152+
guard let urlRequest = try? request.asURLRequest() else {
153+
return false
154+
}
155+
return retriedJetpackRequests.contains { retriedRequest in
156+
guard let currentItem = try? retriedRequest.request.asURLRequest() else {
157+
return false
158+
}
159+
return currentItem.url == urlRequest.url &&
160+
currentItem.httpMethod == urlRequest.httpMethod
148161
}
149162
}
150163

Modules/Tests/NetworkingTests/Network/AlamofireNetworkErrorHandlerTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,52 @@ final class AlamofireNetworkErrorHandlerTests: XCTestCase {
560560
// Then
561561
XCTAssertTrue(userDefaults.applicationPasswordUnsupportedList.keys.contains(String(siteID)))
562562
}
563+
564+
func test_concurrent_flagSiteAsUnsupportedForAppPasswordIfNeeded_no_race_condition() {
565+
// Given - test for the race condition fix where multiple threads
566+
// try to remove the same item simultaneously
567+
let expectation = XCTestExpectation(description: "All concurrent flag operations complete without crash")
568+
let threadCount = 50
569+
let siteID: Int64 = 999
570+
let jetpackRequest = createJetpackRequest(siteID: siteID)
571+
let restRequest = createRESTRequest()
572+
let error = createNetworkError()
573+
574+
expectation.expectedFulfillmentCount = threadCount
575+
576+
// Add a single request to the retry list
577+
_ = errorHandler.shouldRetryJetpackRequest(
578+
originalRequest: jetpackRequest,
579+
convertedRequest: restRequest,
580+
failure: error
581+
)
582+
583+
// When - multiple threads concurrently try to flag and remove the SAME item
584+
// This would cause a race condition in the old code where:
585+
// 1. Thread A reads the array, finds index 0
586+
// 2. Thread B reads the array, finds index 0
587+
// 3. Thread A removes at index 0 (succeeds)
588+
// 4. Thread B tries to remove at index 0 (crashes - array is now empty)
589+
let group = DispatchGroup()
590+
for _ in 0..<threadCount {
591+
group.enter()
592+
DispatchQueue.global().async {
593+
// All threads try to remove the same request concurrently
594+
self.errorHandler.flagSiteAsUnsupportedForAppPasswordIfNeeded(
595+
originalRequest: jetpackRequest,
596+
failure: nil
597+
)
598+
group.leave()
599+
expectation.fulfill()
600+
}
601+
}
602+
603+
// Wait for all threads to complete
604+
group.wait()
605+
606+
// Then - no crashes should occur (especially no EXC_BREAKPOINT from array index out of bounds)
607+
wait(for: [expectation], timeout: 5.0)
608+
}
563609
}
564610

565611
// MARK: - Helper Methods

0 commit comments

Comments
 (0)