Skip to content

Commit 24e2e8f

Browse files
committed
feat(network): prevent concurrent token refresh by queueing pending requests
1 parent d136bd8 commit 24e2e8f

1 file changed

Lines changed: 34 additions & 10 deletions

File tree

Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ actor RequestProcessor {
4141
/// This applies to all requests processed by this instance.
4242
private let retryEvaluator: (@Sendable (Error) -> RetryAction)?
4343

44+
/// A shared in-flight refresh task. All requests that need a token refresh
45+
/// will await this single task instead of each triggering their own refresh.
46+
private var pendingRefreshTask: Task<Void, Error>?
47+
4448
// MARK: - Initialization
4549

4650
/// Creates a new `RequestProcessor` instance.
@@ -118,26 +122,46 @@ actor RequestProcessor {
118122
try await interceptor?.adapt(request: &urlRequest, for: session)
119123
}
120124

121-
/// Checks if a request requires a credential refresh and performs it if necessary.
125+
/// Ensures that only a single token refresh is in flight at any given time.
126+
///
127+
/// When the first request detects that a refresh is needed it creates a shared
128+
/// `Task` and stores it in `pendingRefreshTask`. Every subsequent request that
129+
/// arrives while the refresh is still running will simply `await` that same task
130+
/// instead of starting a new one. Once the task completes (successfully or with
131+
/// an error) `pendingRefreshTask` is set to `nil` so that the next refresh
132+
/// cycle can start fresh.
122133
///
123134
/// - Parameters:
124135
/// - urlRequest: The failed or unauthorized request.
125136
/// - response: The received network response.
126137
/// - session: The current `URLSession`.
127-
/// - Returns: `true` if a refresh was triggered, `false` otherwise.
128-
private func refresh(
138+
/// - Returns: `true` if a refresh was triggered or awaited, `false` otherwise.
139+
private func refreshIfNeeded(
129140
urlRequest: URLRequest,
130141
response: Response<some Any>,
131142
session: URLSession
132143
) async throws -> Bool {
133-
guard let interceptor, let response = response.response as? HTTPURLResponse else { return false }
134-
135-
if interceptor.isRequireRefresh(urlRequest, response: response) {
136-
try await interceptor.refresh(urlRequest, with: response, for: session)
144+
guard
145+
let interceptor,
146+
let httpResponse = response.response as? HTTPURLResponse,
147+
interceptor.isRequireRefresh(urlRequest, response: httpResponse)
148+
else { return false }
149+
150+
if let existingTask = pendingRefreshTask {
151+
try await existingTask.value
137152
return true
138153
}
139154

140-
return false
155+
let refreshTask = Task<Void, Error> {
156+
try await interceptor.refresh(urlRequest, with: httpResponse, for: session)
157+
}
158+
159+
pendingRefreshTask = refreshTask
160+
161+
defer { pendingRefreshTask = nil }
162+
163+
try await refreshTask.value
164+
return true
141165
}
142166

143167
/// Wraps a request operation with retry logic provided by the `retryPolicyService`.
@@ -237,13 +261,13 @@ actor RequestProcessor {
237261
response: Response<Data>,
238262
delegate: URLSessionDelegate?
239263
) async throws -> Response<Data> {
240-
guard try await refresh(urlRequest: urlRequest, response: response, session: session) else {
264+
guard try await refreshIfNeeded(urlRequest: urlRequest, response: response, session: session) else {
241265
return response
242266
}
243267

244268
let retryResponse = try await performDataTask(urlRequest: urlRequest, delegate: delegate)
245269

246-
guard try await !refresh(urlRequest: urlRequest, response: retryResponse, session: session) else {
270+
guard try await !refreshIfNeeded(urlRequest: urlRequest, response: retryResponse, session: session) else {
247271
throw AuthenticatorInterceptorError.missingCredential
248272
}
249273

0 commit comments

Comments
 (0)