Alamofire : How to handle errors globally
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)
}
Sylver
Entrepreneur. Tech Advisor. Software Craftsman. CTO @ cresh.eu
Updated on June 21, 2022Comments
-
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 about 9 yearsWoah, 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 about 9 yearsSo, if i get it well, all my network calls should now be done by
manager.startRequest()
exclusively ? -
cnoon about 9 yearsExactly. Any request that goes through the potential 401 refresh flow.
-
Sylver about 9 yearsI have one more question regarding your example. You're using references to self, with
[weak self]
andlet 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 about 9 years
-
Sylver about 9 yearsGreat, 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 about 9 yearsMy guess is that your manager is getting deallocated before the call completes. If you don't have a
[weak self]
, thenself
gets retained and you're able to complete the call. -
Sylver about 9 yearsSure ! 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 about 9 yearsYou 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 about 9 yearsOooh, 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 about 9 yearsBtw, 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 about 9 yearsLet us continue this discussion in chat.
-
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 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 theheaders
parameter on therequest
method if you were not using theRouter
pattern. -
JaviCasa over 8 yearsTop 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 over 8 yearsThe
$0(nil, nil, nil)
just calls all the cached task closures by passing innil
for all the arguments. It's how you can easily restart all the cached tasks. -
user1007522 over 8 yearsit 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 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 over 8 yearsI'm having the problem that the success or failure callback is not called :s
-
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 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 over 8 years@cnoon nm, figured it out. setting
self.isRefreshing = false
before callingcachedTasksCopy.map { $0(nil, nil, nil) }
fixes my issue. Restarting the cache before setting state was causing the cachedTask to continually get re-cached. -
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 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 about 8 years@thesummersign the values are only passed in a refresh failure case.
-
Steve P. Sharpe about 8 yearsHey @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 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 over 7 yearsI 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 over 7 yearsFor 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 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.