Alamofire : How to handle errors globally

16,812

Solution 1

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.

Solution 2

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)
}
Share:
16,812
Sylver
Author by

Sylver

Entrepreneur. Tech Advisor. Software Craftsman. CTO @ cresh.eu

Updated on June 21, 2022

Comments

  • Sylver
    Sylver almost 2 years

    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 ?

  • Sylver
    Sylver about 9 years
    Woah, that's a very well detailed answer and exactly what i'm looking for, at least a good way to help me understand all of that and how to design such kind of things. Thank you very much for your answer.
  • Sylver
    Sylver about 9 years
    So, if i get it well, all my network calls should now be done by manager.startRequest() exclusively ?
  • cnoon
    cnoon about 9 years
    Exactly. Any request that goes through the potential 401 refresh flow.
  • Sylver
    Sylver about 9 years
    I have one more question regarding your example. You're using references to self, with [weak self] and let strongSelf = self, and i don't really understand what it's suppose to mean. I think i get it about the strongSelf : making a retain reference to self for calling functions in the same instance. But not about the [weak self] along with a parameter in the blocks ?
  • cnoon
    cnoon about 9 years
    Good question, this article and this one cover everything you need to know. You need to weakify / strongify to avoid unwanted retain cycles.
  • Sylver
    Sylver about 9 years
    Great, thank you, that something i need to work more on to understand. I tried to do a manager like you showed me, but with the [weak self] in the response block, self is nil and i can't catch the 401. If i remove it, it goes through and everything is fine. Am i missing something here ? Why the [weak self] you put here does the inverse effect it is supposed to do ?
  • cnoon
    cnoon about 9 years
    My guess is that your manager is getting deallocated before the call completes. If you don't have a [weak self], then self gets retained and you're able to complete the call.
  • Sylver
    Sylver about 9 years
    Sure ! But i suppose you had a good reason to put a weak reference here, so why it doesn't work as expected and gets deallocated ? Is it ok to do without ? It's supposed to avoid a cycling reference right ?
  • cnoon
    cnoon about 9 years
    You need to retain the Manager so that it doesn't go out of scope. What I mean by that is that you should maybe use a singleton pattern on the Manager, or have it stored as a property inside a larger object that is possibly a singleton so that it never gets deallocated. You need to keep the Manager instance in memory at all times so you can properly have the refreshed tasks. If the Manager gets deallocated and you always end up creating new ones, refreshing will never work properly.
  • Sylver
    Sylver about 9 years
    Oooh, sure, i get it. I need to use a shared instance of the manager, like Alamofire.Manager.sharedInstance, ok so i can do it without the weak ref then
  • Sylver
    Sylver about 9 years
    Btw, why in the map of the cachedTasks you call it with (nil, nil, nil) and what's the point to define it like that if we don't pass arguments to it ?
  • cnoon
    cnoon about 9 years
  • OgreSwamp
    OgreSwamp over 8 years
    @cnoon thanks! Quick question. There is a comment about appending tokens in parameters in your example. But in case of OAuth2 parameters goes to HTTP header. But I do that in routers (extensions of URLRequestConvertible). I don't think I can append header in your example as returned request is immutable. Any idea if it is possible to add auth token to the HTTP header in your manager or I should handle that in routers?
  • cnoon
    cnoon over 8 years
    @OgreSwamp you'll need to do that in the Router if that's the way you are building your requests. In this example you could use the headers parameter on the request method if you were not using the Router pattern.
  • JaviCasa
    JaviCasa over 8 years
    Top notch answer. However, the chat seems to have disappeared, and I'm really wondering the reason for the $0(nil,nil,nil). Could you please explain its usage?
  • cnoon
    cnoon over 8 years
    The $0(nil, nil, nil) just calls all the cached task closures by passing in nil for all the arguments. It's how you can easily restart all the cached tasks.
  • user1007522
    user1007522 over 8 years
    it seems to me that if you execute this one that this all happens on the main thread? Because we are waiting for the request to complete? Can we just solve this by working with a callback or is there a better way?
  • the_critic
    the_critic over 8 years
    @cnoon Correct me if I'm wrong, but I think this should be a singleton. Alamofire's built-in request method also executes requests on a singleton manager instance and I don't see a reason why you wouldn't want to do it here as well.
  • user1007522
    user1007522 over 8 years
    I'm having the problem that the success or failure callback is not called :s
  • oflannabhra
    oflannabhra over 8 years
    @cnoon Thanks so much for this! I was just banging my head against a wall and had begun writing out something very similar, but kept stumbling in my implementation. This should be on the Alamofire README.md imo. One of the best answers I've seen on SO.
  • oflannabhra
    oflannabhra over 8 years
    @cnoon I'm also having an issue restarting the cached requests... I'm sure I properly cache them, but calling them back does not result in them succeeding or failing...
  • oflannabhra
    oflannabhra over 8 years
    @cnoon nm, figured it out. setting self.isRefreshing = false before calling cachedTasksCopy.map { $0(nil, nil, nil) } fixes my issue. Restarting the cache before setting state was causing the cachedTask to continually get re-cached.
  • Faruk
    Faruk about 8 years
    @cnoon First of all, thanks for this awesome Alamofire framework :) I am kinda newbie to swift and have a very simple question. How can I embed this class to my project? Can you please explain more detailed?
  • thesummersign
    thesummersign about 8 years
    @cnoon I just want to understand why do we need to have the 3 arguments in the CashedTask closure while we are not using them. the only way we are invoking the closure is with $0(nil, nil, nil)
  • cnoon
    cnoon about 8 years
    @thesummersign the values are only passed in a refresh failure case.
  • Steve P. Sharpe
    Steve P. Sharpe about 8 years
    Hey @cnoon, thanks for the code example it really helps. I almost have it working, however the very first isRefreshing is always false. This block here. if self.isRefreshing { self.cachedTasks.append(cachedTask) return nil }
  • GuillermoMP
    GuillermoMP almost 8 years
    @cnoon You state that this prevents multiple-call issue, but if i execute 2+ requests at the same they will all pass the ´isRefreshing´ condition and get queried so at some point more than 1 of them can hit the 401 and call the ´refreshTokens()´. Additional checkings need to be performed to avoid this.
  • cnoon
    cnoon over 7 years
    I agree @Mindhavok. We're going to do you one better though. We're currently working on building a refresh system directly into Alamofire. We're hoping to have this ship as part of Alamofire 4.0.0. Stay tuned...
  • cnoon
    cnoon over 7 years
    For everyone on this thread, I just updated it with info about Alamofire 4.0 having direct support built in for handling refresh and authentication systems.
  • Ashildr
    Ashildr over 7 years
    @cnoon can you please take a look at stackoverflow.com/a/40238295/3150830.I don't know why.Please delete this comment after that.