From a98f28a5fe8639c9c8a48b948e3adf4741d7555f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Mu=CC=88hleder?= Date: Tue, 16 May 2023 12:31:19 +0200 Subject: [PATCH 1/2] Use actor for AuthHandler (token refresh) --- .../Core/Sources/Networking/AuthHandler.swift | 132 ++++++++---------- 1 file changed, 61 insertions(+), 71 deletions(-) diff --git a/{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift b/{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift index 7f3f30d..193fb28 100644 --- a/{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift +++ b/{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift @@ -1,97 +1,87 @@ -import Foundation import Alamofire import Fetch +import Foundation import Models public class AuthHandler: RequestInterceptor { - - private typealias RefreshCompletion = (_ credentials: Credentials?, _ statusCode: Int?) -> Void - private typealias RequestRetryCompletion = (Alamofire.RetryResult) -> Void - - private static let apiLogger = APILogger(verbose: true) - - private let session: Session = { - let configuration = URLSessionConfiguration.default - return Session(configuration: configuration, eventMonitors: [apiLogger]) - }() - - private let lock = NSLock() - private let queue = DispatchQueue(label: "network.auth.queue") - private var isRefreshing = false - private var requestsToRetry: [RequestRetryCompletion] = [] - + private let actor: CredentialsRefreshActor = .init() + // MARK: - RequestAdapter - - public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + + public func adapt(_ urlRequest: URLRequest, for _: Session, completion: @escaping (Result) -> Void) { var urlRequest = urlRequest if let accessToken = CredentialsController.shared.currentCredentials?.accessToken { urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") } completion(.success(urlRequest)) } - + // MARK: - RequestRetrier - - public func retry(_ request: Alamofire.Request, for session: Alamofire.Session, dueTo error: Error, completion: @escaping (Alamofire.RetryResult) -> Void) { - lock.lock() ; defer { lock.unlock() } - + + public func retry(_ request: Alamofire.Request, for _: Alamofire.Session, dueTo _: Error, completion: @escaping (Alamofire.RetryResult) -> Void) { guard let response = request.task?.response as? HTTPURLResponse, - let refreshToken = CredentialsController.shared.currentCredentials?.refreshToken, - response.statusCode == 401 else { - completion(.doNotRetry) - return + let refreshToken = CredentialsController.shared.currentCredentials?.refreshToken, + response.statusCode == 401 else { + completion(.doNotRetry) + return } - - requestsToRetry.append(completion) - - if !isRefreshing { - refreshCredentials(refreshToken) { [weak self] (credentials, statusCode) in - guard let self = self else { return } - - self.lock.lock() ; defer { self.lock.unlock() } - - if let credentials = credentials { - CredentialsController.shared.currentCredentials = credentials - self.requestsToRetry.forEach { $0(.retry) } - } else { - if statusCode == 401 { - CredentialsController.shared.currentCredentials = nil - } - self.requestsToRetry.forEach { $0(.doNotRetry) } - } - - self.requestsToRetry.removeAll() - } + + Task { + await actor.addRequest(completion) + await actor.refresh(refreshToken: refreshToken) } } +} + +actor CredentialsRefreshActor { + typealias RequestRetryCompletion = (Alamofire.RetryResult) -> Void + + private static let apiLogger = APILogger(verbose: true) - // MARK: - Private - Refresh Tokens - - private func refreshCredentials(_ refreshToken: String, completion: @escaping RefreshCompletion) { + private var isRefreshing = false + private var requestsToRetry: [RequestRetryCompletion] = [] + private let session: Session = { + let configuration = URLSessionConfiguration.default + return Session(configuration: configuration, eventMonitors: [apiLogger]) + }() + + func addRequest(_ request: @escaping RequestRetryCompletion) async { + requestsToRetry.append(request) + } + + func refresh(refreshToken: String) async { guard !isRefreshing else { return } - + isRefreshing = true - + + defer { + requestsToRetry.removeAll() + isRefreshing = false + } + guard let urlRequest = try? API.Auth.tokenRefresh(refreshToken).asURLRequest() else { - completion(nil, nil) + requestsToRetry.forEach { $0(.doNotRetry) } return } - - session - .request(urlRequest) + + let response = await AF.request(urlRequest) .validate() - .responseDecodable(queue: queue, completionHandler: { [weak self] (response: DataResponse) in - guard let self = self else { return } - - let statusCode = response.response?.statusCode - - switch response.result { - case .success(let credentials): - completion(credentials, statusCode) - case .failure: - completion(nil, statusCode) - } - self.isRefreshing = false - }) + .serializingDecodable(Credentials.self) + .response + + let statusCode = response.response?.statusCode + + switch response.result { + case .success(let credentials): + CredentialsController.shared.currentCredentials = credentials + requestsToRetry.forEach { $0(.retry) } + + case .failure: + if statusCode == 401 { + CredentialsController.shared.currentCredentials = nil + } + requestsToRetry.forEach { $0(.doNotRetry) } + } + isRefreshing = false } } From 6974295917cb3039daa617ded386a24a81d915a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Mu=CC=88hleder?= Date: Tue, 16 May 2023 12:48:02 +0200 Subject: [PATCH 2/2] Remove isRefreshing = false (handled in defer) --- .../Modules/Core/Sources/Networking/AuthHandler.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift b/{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift index 193fb28..c3b6c27 100644 --- a/{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift +++ b/{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift @@ -82,6 +82,5 @@ actor CredentialsRefreshActor { } requestsToRetry.forEach { $0(.doNotRetry) } } - isRefreshing = false } }