38

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 ?

Community
  • 1
  • 1
Sylver
  • 2,285
  • 3
  • 29
  • 40

2 Answers2

98

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.

cnoon
  • 16,575
  • 7
  • 58
  • 66
  • 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 Feb 26 '15 at 18:23
  • So, if i get it well, all my network calls should now be done by `manager.startRequest()` exclusively ? – Sylver Feb 26 '15 at 18:32
  • 1
    Exactly. Any request that goes through the potential 401 refresh flow. – cnoon Feb 27 '15 at 04:13
  • 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 ? – Sylver Feb 27 '15 at 17:52
  • 1
    Good question, this [article](https://blackpixel.com/writing/2014/03/capturing-myself.html) and this [one](http://stackoverflow.com/questions/24320347/shall-we-always-use-unowned-self-inside-closure-in-swift) cover everything you need to know. You need to weakify / strongify to avoid unwanted retain cycles. – cnoon Feb 27 '15 at 19:10
  • 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 ? – Sylver Feb 27 '15 at 22:30
  • 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. – cnoon Feb 28 '15 at 02:01
  • 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 ? – Sylver Feb 28 '15 at 09:47
  • 1
    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. – cnoon Feb 28 '15 at 17:38
  • 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 Feb 28 '15 at 17:40
  • 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 ? – Sylver Feb 28 '15 at 17:41
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/71955/discussion-between-cnoon-and-sylver). – cnoon Feb 28 '15 at 17:48
  • @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? – OgreSwamp Sep 07 '15 at 22:16
  • @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. – cnoon Sep 08 '15 at 15:38
  • 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? – JaviCasa Dec 17 '15 at 03:08
  • 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. – cnoon Dec 17 '15 at 06:46
  • 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? – user1007522 Jan 21 '16 at 15:14
  • @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. – the_critic Jan 28 '16 at 11:24
  • I'm having the problem that the success or failure callback is not called :s – user1007522 Feb 01 '16 at 14:54
  • @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 Feb 04 '16 at 19:28
  • @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 Feb 05 '16 at 19:26
  • 1
    @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. – oflannabhra Feb 05 '16 at 19:39
  • 1
    @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? – Faruk Feb 17 '16 at 21:38
  • @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)` – geekay Mar 28 '16 at 12:18
  • @thesummersign the values are only passed in a refresh failure case. – cnoon Mar 31 '16 at 02:59
  • 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 }` – Steve P. Sharpe Apr 07 '16 at 20:23
  • @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. – GuillermoMP Jun 30 '16 at 19:52
  • 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 Aug 24 '16 at 14:23
  • 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. – cnoon Oct 02 '16 at 18:30
  • @cnoon can you please take a look at http://stackoverflow.com/a/40238295/3150830.I don't know why.Please delete this comment after that. – Ashildr Oct 25 '16 at 11:16
5

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)
}
Hamed safari
  • 295
  • 4
  • 6