简体   繁体   中英

Alamofire : How to handle errors globally

My question is quite similar to this one, but for Alamofire : AFNetworking: Handle error globally and repeat request

How to be able to catch globally an error (typically a 401) and handle it before other requests are made (and eventually failed if not managed) ?

I was thinking of chaining a custom response handler, but that's silly to do it on each request of the app.
Maybe subclassing, but which class should i subclass to handle that ?

Handling refresh for 401 responses in an oauth flow is quite complicated given the parallel nature of NSURLSessions. I have spent quite some time building an internal solution that has worked extremely well for us. The following is a very high level extraction of the general idea of how it was implemented.

import Foundation
import Alamofire

public class AuthorizationManager: Manager {
    public typealias NetworkSuccessHandler = (AnyObject?) -> Void
    public typealias NetworkFailureHandler = (NSHTTPURLResponse?, AnyObject?, NSError) -> Void

    private typealias CachedTask = (NSHTTPURLResponse?, AnyObject?, NSError?) -> Void

    private var cachedTasks = Array<CachedTask>()
    private var isRefreshing = false

    public func startRequest(
        method method: Alamofire.Method,
        URLString: URLStringConvertible,
        parameters: [String: AnyObject]?,
        encoding: ParameterEncoding,
        success: NetworkSuccessHandler?,
        failure: NetworkFailureHandler?) -> Request?
    {
        let cachedTask: CachedTask = { [weak self] URLResponse, data, error in
            guard let strongSelf = self else { return }

            if let error = error {
                failure?(URLResponse, data, error)
            } else {
                strongSelf.startRequest(
                    method: method,
                    URLString: URLString,
                    parameters: parameters,
                    encoding: encoding,
                    success: success,
                    failure: failure
                )
            }
        }

        if self.isRefreshing {
            self.cachedTasks.append(cachedTask)
            return nil
        }

        // Append your auth tokens here to your parameters

        let request = self.request(method, URLString, parameters: parameters, encoding: encoding)

        request.response { [weak self] request, response, data, error in
            guard let strongSelf = self else { return }

            if let response = response where response.statusCode == 401 {
                strongSelf.cachedTasks.append(cachedTask)
                strongSelf.refreshTokens()
                return
            }

            if let error = error {
                failure?(response, data, error)
            } else {
                success?(data)
            }
        }

        return request
    }

    func refreshTokens() {
        self.isRefreshing = true

        // Make the refresh call and run the following in the success closure to restart the cached tasks

        let cachedTaskCopy = self.cachedTasks
        self.cachedTasks.removeAll()
        cachedTaskCopy.map { $0(nil, nil, nil) }

        self.isRefreshing = false
    }
}

The most important thing here to remember is that you don't want to run a refresh call for every 401 that comes back. A large number of requests can be racing at the same time. Therefore, you want to act on the first 401, and queue all the additional requests until the 401 has succeeded. The solution I outlined above does exactly that. Any data task that is started through the startRequest method will automatically get refreshed if it hits a 401.

Some other important things to note here that are not accounted for in this very simplified example are:

  • Thread-safety
  • Guaranteed success or failure closure calls
  • Storing and fetching the oauth tokens
  • Parsing the response
  • Casting the parsed response to the appropriate type (generics)

Hopefully this helps shed some light.


Update

We have now released 🔥🔥 Alamofire 4.0 🔥🔥 which adds the RequestAdapter and RequestRetrier protocols allowing you to easily build your own authentication system regardless of the authorization implementation details! For more information, please refer to our README which has a complete example of how you could implement on OAuth2 system into your app.

Full Disclosure: The example in the README is only meant to be used as an example. Please please please do NOT just go and copy-paste the code into a production application.

in Alamofire 5 you can use RequestInterceptor Here is my error handling for 401 error in one of my projects, every requests that I pass the EnvironmentInterceptor to it the func of retry will be called if the request get to error and also the adapt func can help you to add default value to your requests

struct EnvironmentInterceptor: RequestInterceptor {

func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (AFResult<URLRequest>) -> Void) {
    var adaptedRequest = urlRequest
    guard let token = KeychainWrapper.standard.string(forKey: KeychainsKeys.token.rawValue) else {
        completion(.success(adaptedRequest))
        return
    }
    adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: HTTPHeaderField.authentication.rawValue)
    completion(.success(adaptedRequest))
}

func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
    if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
        //get token

        guard let refreshToken = KeychainWrapper.standard.string(forKey: KeychainsKeys.refreshToken.rawValue) else {
            completion(.doNotRetryWithError(error))
            return
        }

        APIDriverAcountClient.refreshToken(refreshToken: refreshToken) { res in
            switch res {
            case .success(let response):
                let saveAccessToken: Bool = KeychainWrapper.standard.set(response.accessToken, forKey: KeychainsKeys.token.rawValue)
                let saveRefreshToken: Bool = KeychainWrapper.standard.set(response.refreshToken, forKey: KeychainsKeys.refreshToken.rawValue)
                let saveUserId: Bool = KeychainWrapper.standard.set(response.userId, forKey: KeychainsKeys.uId.rawValue)
                print("is accesstoken saved ?: \(saveAccessToken)")
                print("is refreshToken saved ?: \(saveRefreshToken)")
                print("is userID saved ?: \(saveUserId)")
                completion(.retry)
                break
            case .failure(let err):
                //TODO logout
                break

            }

        }
    } else {
        completion(.doNotRetry)
    }
}

and you can use it like this :

@discardableResult
private static func performRequest<T: Decodable>(route: ApiDriverTrip, decoder: JSONDecoder = JSONDecoder(), completion: @escaping (AFResult<T>)->Void) -> DataRequest {

    return AF.request(route, interceptor: EnvironmentInterceptor())
        .responseDecodable (decoder: decoder){ (response: DataResponse<T>) in
         completion(response.result)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM