@@ -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