Reputation: 1251
I'm implementing a way to refresh the session token using OAuth2 with AlamoFire5, and I'm trying to figure out how to solve this scenario:
1 - When some request fail a refreshToken request must start, that must be the only one refreshToken request running at a time. i.e. the other requests that failed should not be retried until that request finishes.
2 - If the refreshToken finishes with an error the app must restarts and all the other request that were waiting must be cancelled.
3 - If the the refreshToken request succeeds the token must be updated and all the other requests waiting must now continue.
I'm using the AlamoFire's RequestInterceptor class to try to solve this problem, and my implementation so far is this one:
final class RequestInterceptor: Alamofire.RequestInterceptor {
private let disposeBag = DisposeBag()
private let lock = NSRecursiveLock()
private var refreshTokenParameters: TokenParameters {
TokenParameters(clientId: "pdappclient",
grantType: "refresh_token",
refreshToken: KeychainManager.shared.refreshToken)
}
private let storage: AccessTokenStorage
init(storage: AccessTokenStorage) {
self.storage = storage
}
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + storage.accessToken, forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
lock.lock()
defer { lock.unlock() }
guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
return completion(.doNotRetryWithError(error))
}
let refreshTokenRequest: Single<TokenResponse> = NetworkManager.shared
.fetchData(fromApi: IdentityServerAPI.token(parameters: self.refreshTokenParameters))
refreshTokenRequest.subscribe(onSuccess: { token in
self.lock.unlock()
self.storage.accessToken = token.accessToken ?? ""
completion(.retry)
}, onError: { error in
self.lock.unlock()
completion(.doNotRetryWithError(error))
}).disposed(by: disposeBag)
}
}
How can I solve this case using the RequestInterceptor?
Upvotes: 4
Views: 468
Reputation: 8327
You can use an array to store the retry closures, for the requests that might occur until the token refresh finishes and a boolean to know that a refresh action is on going.
You will end up with something like this:
final class RequestInterceptor: Alamofire.RequestInterceptor {
private let disposeBag = DisposeBag()
private let lock = NSRecursiveLock()
private var refreshTokenParameters: TokenParameters {
TokenParameters(
clientId: "pdappclient",
grantType: "refresh_token",
refreshToken: KeychainManager.shared.refreshToken
)
}
private let storage: AccessTokenStorage
private var retryQueue = [(RetryResult) -> Void]()
private var isTokenRefreshing = false
init(storage: AccessTokenStorage) {
self.storage = storage
}
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + storage.accessToken, forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
lock.lock()
defer { lock.unlock() }
guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
completion(.doNotRetryWithError(error))
return
}
retryQueue.append(completion)
if !isTokenRefreshing {
isTokenRefreshing = true
let refreshTokenRequest: Single<TokenResponse> = NetworkManager.shared
.fetchData(fromApi: IdentityServerAPI.token(parameters: self.refreshTokenParameters))
refreshTokenRequest.subscribe(onSuccess: { token in
self.lock.lock()
defer { self.lock.unlock() }
self.storage.accessToken = token.accessToken ?? ""
self.retryQueue.forEach { $0(.retry) }
self.retryQueue.removeAll()
self.isTokenRefreshing = false
}, onError: { error in
self.lock.lock()
defer { self.lock.unlock() }
self.retryQueue.forEach { $0(.doNotRetryWithError(error)) }
self.retryQueue.removeAll()
self.isTokenRefreshing = false
}).disposed(by: disposeBag)
}
}
}
Note that as defer documentation states:
A
defer
statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.
So, the closure of the first defer
statement will be executed before onSuccess
or onError
closures.
That's why we need to lock again the source within onSuccess
and onError
closures.
Upvotes: 1