1 |
efrain |
1 |
//
|
|
|
2 |
// AuthenticationInterceptor.swift
|
|
|
3 |
//
|
|
|
4 |
// Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/)
|
|
|
5 |
//
|
|
|
6 |
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
7 |
// of this software and associated documentation files (the "Software"), to deal
|
|
|
8 |
// in the Software without restriction, including without limitation the rights
|
|
|
9 |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
10 |
// copies of the Software, and to permit persons to whom the Software is
|
|
|
11 |
// furnished to do so, subject to the following conditions:
|
|
|
12 |
//
|
|
|
13 |
// The above copyright notice and this permission notice shall be included in
|
|
|
14 |
// all copies or substantial portions of the Software.
|
|
|
15 |
//
|
|
|
16 |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
17 |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
18 |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
19 |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
20 |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
21 |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
22 |
// THE SOFTWARE.
|
|
|
23 |
//
|
|
|
24 |
|
|
|
25 |
import Foundation
|
|
|
26 |
|
|
|
27 |
/// Types adopting the `AuthenticationCredential` protocol can be used to authenticate `URLRequest`s.
|
|
|
28 |
///
|
|
|
29 |
/// One common example of an `AuthenticationCredential` is an OAuth2 credential containing an access token used to
|
|
|
30 |
/// authenticate all requests on behalf of a user. The access token generally has an expiration window of 60 minutes
|
|
|
31 |
/// which will then require a refresh of the credential using the refresh token to generate a new access token.
|
|
|
32 |
public protocol AuthenticationCredential {
|
|
|
33 |
/// Whether the credential requires a refresh. This property should always return `true` when the credential is
|
|
|
34 |
/// expired. It is also wise to consider returning `true` when the credential will expire in several seconds or
|
|
|
35 |
/// minutes depending on the expiration window of the credential.
|
|
|
36 |
///
|
|
|
37 |
/// For example, if the credential is valid for 60 minutes, then it would be wise to return `true` when the
|
|
|
38 |
/// credential is only valid for 5 minutes or less. That ensures the credential will not expire as it is passed
|
|
|
39 |
/// around backend services.
|
|
|
40 |
var requiresRefresh: Bool { get }
|
|
|
41 |
}
|
|
|
42 |
|
|
|
43 |
// MARK: -
|
|
|
44 |
|
|
|
45 |
/// Types adopting the `Authenticator` protocol can be used to authenticate `URLRequest`s with an
|
|
|
46 |
/// `AuthenticationCredential` as well as refresh the `AuthenticationCredential` when required.
|
|
|
47 |
public protocol Authenticator: AnyObject {
|
|
|
48 |
/// The type of credential associated with the `Authenticator` instance.
|
|
|
49 |
associatedtype Credential: AuthenticationCredential
|
|
|
50 |
|
|
|
51 |
/// Applies the `Credential` to the `URLRequest`.
|
|
|
52 |
///
|
|
|
53 |
/// In the case of OAuth2, the access token of the `Credential` would be added to the `URLRequest` as a Bearer
|
|
|
54 |
/// token to the `Authorization` header.
|
|
|
55 |
///
|
|
|
56 |
/// - Parameters:
|
|
|
57 |
/// - credential: The `Credential`.
|
|
|
58 |
/// - urlRequest: The `URLRequest`.
|
|
|
59 |
func apply(_ credential: Credential, to urlRequest: inout URLRequest)
|
|
|
60 |
|
|
|
61 |
/// Refreshes the `Credential` and executes the `completion` closure with the `Result` once complete.
|
|
|
62 |
///
|
|
|
63 |
/// Refresh can be called in one of two ways. It can be called before the `Request` is actually executed due to
|
|
|
64 |
/// a `requiresRefresh` returning `true` during the adapt portion of the `Request` creation process. It can also
|
|
|
65 |
/// be triggered by a failed `Request` where the authentication server denied access due to an expired or
|
|
|
66 |
/// invalidated access token.
|
|
|
67 |
///
|
|
|
68 |
/// In the case of OAuth2, this method would use the refresh token of the `Credential` to generate a new
|
|
|
69 |
/// `Credential` using the authentication service. Once complete, the `completion` closure should be called with
|
|
|
70 |
/// the new `Credential`, or the error that occurred.
|
|
|
71 |
///
|
|
|
72 |
/// In general, if the refresh call fails with certain status codes from the authentication server (commonly a 401),
|
|
|
73 |
/// the refresh token in the `Credential` can no longer be used to generate a valid `Credential`. In these cases,
|
|
|
74 |
/// you will need to reauthenticate the user with their username / password.
|
|
|
75 |
///
|
|
|
76 |
/// Please note, these are just general examples of common use cases. They are not meant to solve your specific
|
|
|
77 |
/// authentication server challenges. Please work with your authentication server team to ensure your
|
|
|
78 |
/// `Authenticator` logic matches their expectations.
|
|
|
79 |
///
|
|
|
80 |
/// - Parameters:
|
|
|
81 |
/// - credential: The `Credential` to refresh.
|
|
|
82 |
/// - session: The `Session` requiring the refresh.
|
|
|
83 |
/// - completion: The closure to be executed once the refresh is complete.
|
|
|
84 |
func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void)
|
|
|
85 |
|
|
|
86 |
/// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`.
|
|
|
87 |
///
|
|
|
88 |
/// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `false`
|
|
|
89 |
/// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then you
|
|
|
90 |
/// will need to work with your authentication server team to understand how to identify when this occurs.
|
|
|
91 |
///
|
|
|
92 |
/// In the case of OAuth2, where an authentication server can invalidate credentials, you will need to inspect the
|
|
|
93 |
/// `HTTPURLResponse` or possibly the `Error` for when this occurs. This is commonly handled by the authentication
|
|
|
94 |
/// server returning a 401 status code and some additional header to indicate an OAuth2 failure occurred.
|
|
|
95 |
///
|
|
|
96 |
/// It is very important to understand how your authentication server works to be able to implement this correctly.
|
|
|
97 |
/// For example, if your authentication server returns a 401 when an OAuth2 error occurs, and your downstream
|
|
|
98 |
/// service also returns a 401 when you are not authorized to perform that operation, how do you know which layer
|
|
|
99 |
/// of the backend returned you a 401? You do not want to trigger a refresh unless you know your authentication
|
|
|
100 |
/// server is actually the layer rejecting the request. Again, work with your authentication server team to understand
|
|
|
101 |
/// how to identify an OAuth2 401 error vs. a downstream 401 error to avoid endless refresh loops.
|
|
|
102 |
///
|
|
|
103 |
/// - Parameters:
|
|
|
104 |
/// - urlRequest: The `URLRequest`.
|
|
|
105 |
/// - response: The `HTTPURLResponse`.
|
|
|
106 |
/// - error: The `Error`.
|
|
|
107 |
///
|
|
|
108 |
/// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise.
|
|
|
109 |
func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool
|
|
|
110 |
|
|
|
111 |
/// Determines whether the `URLRequest` is authenticated with the `Credential`.
|
|
|
112 |
///
|
|
|
113 |
/// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `true`
|
|
|
114 |
/// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then
|
|
|
115 |
/// read on.
|
|
|
116 |
///
|
|
|
117 |
/// When an authentication server can invalidate credentials, it means that you may have a non-expired credential
|
|
|
118 |
/// that appears to be valid, but will be rejected by the authentication server when used. Generally when this
|
|
|
119 |
/// happens, a number of requests are all sent when the application is foregrounded, and all of them will be
|
|
|
120 |
/// rejected by the authentication server in the order they are received. The first failed request will trigger a
|
|
|
121 |
/// refresh internally, which will update the credential, and then retry all the queued requests with the new
|
|
|
122 |
/// credential. However, it is possible that some of the original requests will not return from the authentication
|
|
|
123 |
/// server until the refresh has completed. This is where this method comes in.
|
|
|
124 |
///
|
|
|
125 |
/// When the authentication server rejects a credential, we need to check to make sure we haven't refreshed the
|
|
|
126 |
/// credential while the request was in flight. If it has already refreshed, then we don't need to trigger an
|
|
|
127 |
/// additional refresh. If it hasn't refreshed, then we need to refresh.
|
|
|
128 |
///
|
|
|
129 |
/// Now that it is understood how the result of this method is used in the refresh lifecyle, let's walk through how
|
|
|
130 |
/// to implement it. You should return `true` in this method if the `URLRequest` is authenticated in a way that
|
|
|
131 |
/// matches the values in the `Credential`. In the case of OAuth2, this would mean that the Bearer token in the
|
|
|
132 |
/// `Authorization` header of the `URLRequest` matches the access token in the `Credential`. If it matches, then we
|
|
|
133 |
/// know the `Credential` was used to authenticate the `URLRequest` and should return `true`. If the Bearer token
|
|
|
134 |
/// did not match the access token, then you should return `false`.
|
|
|
135 |
///
|
|
|
136 |
/// - Parameters:
|
|
|
137 |
/// - urlRequest: The `URLRequest`.
|
|
|
138 |
/// - credential: The `Credential`.
|
|
|
139 |
///
|
|
|
140 |
/// - Returns: `true` if the `URLRequest` is authenticated with the `Credential`, `false` otherwise.
|
|
|
141 |
func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool
|
|
|
142 |
}
|
|
|
143 |
|
|
|
144 |
// MARK: -
|
|
|
145 |
|
|
|
146 |
/// Represents various authentication failures that occur when using the `AuthenticationInterceptor`. All errors are
|
|
|
147 |
/// still vended from Alamofire as `AFError` types. The `AuthenticationError` instances will be embedded within
|
|
|
148 |
/// `AFError` `.requestAdaptationFailed` or `.requestRetryFailed` cases.
|
|
|
149 |
public enum AuthenticationError: Error {
|
|
|
150 |
/// The credential was missing so the request could not be authenticated.
|
|
|
151 |
case missingCredential
|
|
|
152 |
/// The credential was refreshed too many times within the `RefreshWindow`.
|
|
|
153 |
case excessiveRefresh
|
|
|
154 |
}
|
|
|
155 |
|
|
|
156 |
// MARK: -
|
|
|
157 |
|
|
|
158 |
/// The `AuthenticationInterceptor` class manages the queuing and threading complexity of authenticating requests.
|
|
|
159 |
/// It relies on an `Authenticator` type to handle the actual `URLRequest` authentication and `Credential` refresh.
|
|
|
160 |
public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator {
|
|
|
161 |
// MARK: Typealiases
|
|
|
162 |
|
|
|
163 |
/// Type of credential used to authenticate requests.
|
|
|
164 |
public typealias Credential = AuthenticatorType.Credential
|
|
|
165 |
|
|
|
166 |
// MARK: Helper Types
|
|
|
167 |
|
|
|
168 |
/// Type that defines a time window used to identify excessive refresh calls. When enabled, prior to executing a
|
|
|
169 |
/// refresh, the `AuthenticationInterceptor` compares the timestamp history of previous refresh calls against the
|
|
|
170 |
/// `RefreshWindow`. If more refreshes have occurred within the refresh window than allowed, the refresh is
|
|
|
171 |
/// cancelled and an `AuthorizationError.excessiveRefresh` error is thrown.
|
|
|
172 |
public struct RefreshWindow {
|
|
|
173 |
/// `TimeInterval` defining the duration of the time window before the current time in which the number of
|
|
|
174 |
/// refresh attempts is compared against `maximumAttempts`. For example, if `interval` is 30 seconds, then the
|
|
|
175 |
/// `RefreshWindow` represents the past 30 seconds. If more attempts occurred in the past 30 seconds than
|
|
|
176 |
/// `maximumAttempts`, an `.excessiveRefresh` error will be thrown.
|
|
|
177 |
public let interval: TimeInterval
|
|
|
178 |
|
|
|
179 |
/// Total refresh attempts allowed within `interval` before throwing an `.excessiveRefresh` error.
|
|
|
180 |
public let maximumAttempts: Int
|
|
|
181 |
|
|
|
182 |
/// Creates a `RefreshWindow` instance from the specified `interval` and `maximumAttempts`.
|
|
|
183 |
///
|
|
|
184 |
/// - Parameters:
|
|
|
185 |
/// - interval: `TimeInterval` defining the duration of the time window before the current time.
|
|
|
186 |
/// - maximumAttempts: The maximum attempts allowed within the `TimeInterval`.
|
|
|
187 |
public init(interval: TimeInterval = 30.0, maximumAttempts: Int = 5) {
|
|
|
188 |
self.interval = interval
|
|
|
189 |
self.maximumAttempts = maximumAttempts
|
|
|
190 |
}
|
|
|
191 |
}
|
|
|
192 |
|
|
|
193 |
private struct AdaptOperation {
|
|
|
194 |
let urlRequest: URLRequest
|
|
|
195 |
let session: Session
|
|
|
196 |
let completion: (Result<URLRequest, Error>) -> Void
|
|
|
197 |
}
|
|
|
198 |
|
|
|
199 |
private enum AdaptResult {
|
|
|
200 |
case adapt(Credential)
|
|
|
201 |
case doNotAdapt(AuthenticationError)
|
|
|
202 |
case adaptDeferred
|
|
|
203 |
}
|
|
|
204 |
|
|
|
205 |
private struct MutableState {
|
|
|
206 |
var credential: Credential?
|
|
|
207 |
|
|
|
208 |
var isRefreshing = false
|
|
|
209 |
var refreshTimestamps: [TimeInterval] = []
|
|
|
210 |
var refreshWindow: RefreshWindow?
|
|
|
211 |
|
|
|
212 |
var adaptOperations: [AdaptOperation] = []
|
|
|
213 |
var requestsToRetry: [(RetryResult) -> Void] = []
|
|
|
214 |
}
|
|
|
215 |
|
|
|
216 |
// MARK: Properties
|
|
|
217 |
|
|
|
218 |
/// The `Credential` used to authenticate requests.
|
|
|
219 |
public var credential: Credential? {
|
|
|
220 |
get { $mutableState.credential }
|
|
|
221 |
set { $mutableState.credential = newValue }
|
|
|
222 |
}
|
|
|
223 |
|
|
|
224 |
let authenticator: AuthenticatorType
|
|
|
225 |
let queue = DispatchQueue(label: "org.alamofire.authentication.inspector")
|
|
|
226 |
|
|
|
227 |
@Protected
|
|
|
228 |
private var mutableState: MutableState
|
|
|
229 |
|
|
|
230 |
// MARK: Initialization
|
|
|
231 |
|
|
|
232 |
/// Creates an `AuthenticationInterceptor` instance from the specified parameters.
|
|
|
233 |
///
|
|
|
234 |
/// A `nil` `RefreshWindow` will result in the `AuthenticationInterceptor` not checking for excessive refresh calls.
|
|
|
235 |
/// It is recommended to always use a `RefreshWindow` to avoid endless refresh cycles.
|
|
|
236 |
///
|
|
|
237 |
/// - Parameters:
|
|
|
238 |
/// - authenticator: The `Authenticator` type.
|
|
|
239 |
/// - credential: The `Credential` if it exists. `nil` by default.
|
|
|
240 |
/// - refreshWindow: The `RefreshWindow` used to identify excessive refresh calls. `RefreshWindow()` by default.
|
|
|
241 |
public init(authenticator: AuthenticatorType,
|
|
|
242 |
credential: Credential? = nil,
|
|
|
243 |
refreshWindow: RefreshWindow? = RefreshWindow()) {
|
|
|
244 |
self.authenticator = authenticator
|
|
|
245 |
mutableState = MutableState(credential: credential, refreshWindow: refreshWindow)
|
|
|
246 |
}
|
|
|
247 |
|
|
|
248 |
// MARK: Adapt
|
|
|
249 |
|
|
|
250 |
public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
|
|
|
251 |
let adaptResult: AdaptResult = $mutableState.write { mutableState in
|
|
|
252 |
// Queue the adapt operation if a refresh is already in place.
|
|
|
253 |
guard !mutableState.isRefreshing else {
|
|
|
254 |
let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
|
|
|
255 |
mutableState.adaptOperations.append(operation)
|
|
|
256 |
return .adaptDeferred
|
|
|
257 |
}
|
|
|
258 |
|
|
|
259 |
// Throw missing credential error is the credential is missing.
|
|
|
260 |
guard let credential = mutableState.credential else {
|
|
|
261 |
let error = AuthenticationError.missingCredential
|
|
|
262 |
return .doNotAdapt(error)
|
|
|
263 |
}
|
|
|
264 |
|
|
|
265 |
// Queue the adapt operation and trigger refresh operation if credential requires refresh.
|
|
|
266 |
guard !credential.requiresRefresh else {
|
|
|
267 |
let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
|
|
|
268 |
mutableState.adaptOperations.append(operation)
|
|
|
269 |
refresh(credential, for: session, insideLock: &mutableState)
|
|
|
270 |
return .adaptDeferred
|
|
|
271 |
}
|
|
|
272 |
|
|
|
273 |
return .adapt(credential)
|
|
|
274 |
}
|
|
|
275 |
|
|
|
276 |
switch adaptResult {
|
|
|
277 |
case let .adapt(credential):
|
|
|
278 |
var authenticatedRequest = urlRequest
|
|
|
279 |
authenticator.apply(credential, to: &authenticatedRequest)
|
|
|
280 |
completion(.success(authenticatedRequest))
|
|
|
281 |
|
|
|
282 |
case let .doNotAdapt(adaptError):
|
|
|
283 |
completion(.failure(adaptError))
|
|
|
284 |
|
|
|
285 |
case .adaptDeferred:
|
|
|
286 |
// No-op: adapt operation captured during refresh.
|
|
|
287 |
break
|
|
|
288 |
}
|
|
|
289 |
}
|
|
|
290 |
|
|
|
291 |
// MARK: Retry
|
|
|
292 |
|
|
|
293 |
public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
|
|
|
294 |
// Do not attempt retry if there was not an original request and response from the server.
|
|
|
295 |
guard let urlRequest = request.request, let response = request.response else {
|
|
|
296 |
completion(.doNotRetry)
|
|
|
297 |
return
|
|
|
298 |
}
|
|
|
299 |
|
|
|
300 |
// Do not attempt retry unless the `Authenticator` verifies failure was due to authentication error (i.e. 401 status code).
|
|
|
301 |
guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else {
|
|
|
302 |
completion(.doNotRetry)
|
|
|
303 |
return
|
|
|
304 |
}
|
|
|
305 |
|
|
|
306 |
// Do not attempt retry if there is no credential.
|
|
|
307 |
guard let credential = credential else {
|
|
|
308 |
let error = AuthenticationError.missingCredential
|
|
|
309 |
completion(.doNotRetryWithError(error))
|
|
|
310 |
return
|
|
|
311 |
}
|
|
|
312 |
|
|
|
313 |
// Retry the request if the `Authenticator` verifies it was authenticated with a previous credential.
|
|
|
314 |
guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else {
|
|
|
315 |
completion(.retry)
|
|
|
316 |
return
|
|
|
317 |
}
|
|
|
318 |
|
|
|
319 |
$mutableState.write { mutableState in
|
|
|
320 |
mutableState.requestsToRetry.append(completion)
|
|
|
321 |
|
|
|
322 |
guard !mutableState.isRefreshing else { return }
|
|
|
323 |
|
|
|
324 |
refresh(credential, for: session, insideLock: &mutableState)
|
|
|
325 |
}
|
|
|
326 |
}
|
|
|
327 |
|
|
|
328 |
// MARK: Refresh
|
|
|
329 |
|
|
|
330 |
private func refresh(_ credential: Credential, for session: Session, insideLock mutableState: inout MutableState) {
|
|
|
331 |
guard !isRefreshExcessive(insideLock: &mutableState) else {
|
|
|
332 |
let error = AuthenticationError.excessiveRefresh
|
|
|
333 |
handleRefreshFailure(error, insideLock: &mutableState)
|
|
|
334 |
return
|
|
|
335 |
}
|
|
|
336 |
|
|
|
337 |
mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime)
|
|
|
338 |
mutableState.isRefreshing = true
|
|
|
339 |
|
|
|
340 |
// Dispatch to queue to hop out of the lock in case authenticator.refresh is implemented synchronously.
|
|
|
341 |
queue.async {
|
|
|
342 |
self.authenticator.refresh(credential, for: session) { result in
|
|
|
343 |
self.$mutableState.write { mutableState in
|
|
|
344 |
switch result {
|
|
|
345 |
case let .success(credential):
|
|
|
346 |
self.handleRefreshSuccess(credential, insideLock: &mutableState)
|
|
|
347 |
case let .failure(error):
|
|
|
348 |
self.handleRefreshFailure(error, insideLock: &mutableState)
|
|
|
349 |
}
|
|
|
350 |
}
|
|
|
351 |
}
|
|
|
352 |
}
|
|
|
353 |
}
|
|
|
354 |
|
|
|
355 |
private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool {
|
|
|
356 |
guard let refreshWindow = mutableState.refreshWindow else { return false }
|
|
|
357 |
|
|
|
358 |
let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval
|
|
|
359 |
|
|
|
360 |
let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in
|
|
|
361 |
guard refreshWindowMin <= refreshTimestamp else { return }
|
|
|
362 |
attempts += 1
|
|
|
363 |
}
|
|
|
364 |
|
|
|
365 |
let isRefreshExcessive = refreshAttemptsWithinWindow >= refreshWindow.maximumAttempts
|
|
|
366 |
|
|
|
367 |
return isRefreshExcessive
|
|
|
368 |
}
|
|
|
369 |
|
|
|
370 |
private func handleRefreshSuccess(_ credential: Credential, insideLock mutableState: inout MutableState) {
|
|
|
371 |
mutableState.credential = credential
|
|
|
372 |
|
|
|
373 |
let adaptOperations = mutableState.adaptOperations
|
|
|
374 |
let requestsToRetry = mutableState.requestsToRetry
|
|
|
375 |
|
|
|
376 |
mutableState.adaptOperations.removeAll()
|
|
|
377 |
mutableState.requestsToRetry.removeAll()
|
|
|
378 |
|
|
|
379 |
mutableState.isRefreshing = false
|
|
|
380 |
|
|
|
381 |
// Dispatch to queue to hop out of the mutable state lock
|
|
|
382 |
queue.async {
|
|
|
383 |
adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) }
|
|
|
384 |
requestsToRetry.forEach { $0(.retry) }
|
|
|
385 |
}
|
|
|
386 |
}
|
|
|
387 |
|
|
|
388 |
private func handleRefreshFailure(_ error: Error, insideLock mutableState: inout MutableState) {
|
|
|
389 |
let adaptOperations = mutableState.adaptOperations
|
|
|
390 |
let requestsToRetry = mutableState.requestsToRetry
|
|
|
391 |
|
|
|
392 |
mutableState.adaptOperations.removeAll()
|
|
|
393 |
mutableState.requestsToRetry.removeAll()
|
|
|
394 |
|
|
|
395 |
mutableState.isRefreshing = false
|
|
|
396 |
|
|
|
397 |
// Dispatch to queue to hop out of the mutable state lock
|
|
|
398 |
queue.async {
|
|
|
399 |
adaptOperations.forEach { $0.completion(.failure(error)) }
|
|
|
400 |
requestsToRetry.forEach { $0(.doNotRetryWithError(error)) }
|
|
|
401 |
}
|
|
|
402 |
}
|
|
|
403 |
}
|