Alamofire : How to handle errors globally
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.
Alamofire: Handle error globally
At the moment you will have to handle that logic in your own custom response
implementation. We (the Alamofire TC) are currently working on ways that we can make this process easier, but it's very difficult to get right without complicating the rest of the APIs. We're still a ways off yet.
With that said, I built an OAuth 2.0 system that handles this process in a different non-open-source library. It is possible, it's just difficult to do. You will need to hook into the response
closure for all requests that could 401. See my answer here for a full breakdown of how to do this.
Hopefully that helps shed some light. Cheers /p>
Alamofire: How to Handle 401 Globally?
It may be the case that your AuthorizationManager is not persisting after its initial attempt to send the request.
Normally it's good practice to avoid the singleton pattern, but this isn't a bad case for it:
public class AuthorizationManager: Manager {
static let shared = AuthorizationManager()
// ...the rest of your class
}
And when calling your request, use this singleton instance rather than instantiating a new AuthorizationManager like
AuthorizationManager.shared.startRequest(method: .POST, ...etc...
I'm guessing this may be the issue, since when you create your AuthorizationManager in both cases there's nothing actively retaining that object. The manager may be created, run the request, and then be deallocated before the cachedTask or even before the completion handling, in which case your guard let strongSelf = self else { return }
would simply return without running any of your completions or cachedTasks.
Hopefully that helps. If that is the problem, then that singleton solution should be very simple.
Error handling in Alamofire
You are are on the right track, but you are going to run into some crucial issues with your current implementation. There are some low level Alamofire things that are going to trip you up that I want to help you out with. Here's an alternative version of your code sample that will be much more effective.
@IBAction func loginPressed(sender: AnyObject) {
let params: [String: AnyObject] = ["email": emailField.text, "password": passwordField.text]
let request = Alamofire.request(.POST, "http://localhost:3000/api/users/authenticate", parameters: params)
request.validate()
request.response { [weak self] request, response, data, error in
if let strongSelf = self {
let data = data as? NSData
if data == nil {
println("Why didn't I get any data back?")
strongSelf.errorLabel.text = "something went wrong"
return
} else if let error = error {
let resultText = NSString(data: data!, encoding: NSUTF8StringEncoding)
println(resultText)
strongSelf.errorLabel.text = "something went wrong"
return
}
var serializationError: NSError?
if let json: AnyObject = NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments, error: &serializationError) {
println("JSON: \(json)")
let welcome = self.storyboard?.instantiateViewControllerWithIdentifier("login") as UINavigationController
self.presentViewController(welcome, animated: true, completion: nil)
} else {
println("Failed to serialize json: \(serializationError)")
}
}
}
}
Validation
First off, the validate
function on the request will validate the following:
HTTPStatusCode
- Has to be 200...299Content-Type
- This header in the response must match theAccept
header in the original request
You can find more information about the validation in Alamofire in the README.
Weakify / Strongify
Make sure to weak self and strong self your closure to make sure you don't end up creating a retain cycle.
Dispatch to Main Queue
Your dispatch calls back to the main queue are not necessary. Alamofire guarantees that your completion handler in the response
and responseJSON
serializers is called on the main queue already. You can actually provide your own dispatch queue to run the serializers on if you wish, but neither your solution or mine are currently doing so making the dispatch calls to the main queue completely unnecessary.
Response Serializer
In your particular case, you don't actually want to use the responseJSON
serializer. If you do, you won't end up getting any data back if you don't pass validation. The reason is that the response from the JSON serialization is what will be returned as the AnyObject
. If serialization fails, the AnyObject
will be nil and you won't be able to read out the data.
Instead, use the response
serializer and try to parse the data manually with NSJSONSerialization
. If that fails, then you can rely on the good ole NSString(data:encoding:)
method to print out the data.
Hopefully this helps shed some light on some fairly complicated ways to get tripped up.
Alamofire doesn't catch error
As Matt has mentioned in the comment, I need to add .validate()
before calling .response()
. This is by design. Final code as below:
Alamofire.request(.GET, "url", parameters: nil)
.validate()
.response { (a,b,data,error) in
// error won't be nil now
// and statusCode will be 401
}
Read this detailed explanation(thanks!) for more information.
Handle timeout with Alamofire
You can compare error._code
and if it is equal to -1001
which is NSURLErrorTimedOut
then you know this was a timeout.
let manager = Alamofire.SessionManager.default
manager.session.configuration.timeoutIntervalForRequest = 120
manager.request("yourUrl", method: .post, parameters: ["parameterKey": "value"])
.responseJSON {
response in
switch (response.result) {
case .success: // succes path
case .failure(let error):
if error._code == NSURLErrorTimedOut {
print("Request timeout!")
}
}
}
Related Topics
Variable Used Before Being Initialized in Function
Uicollectionview Performance - _Updatevisiblecellsnow
Uicollectionview with a Sticky Header
How Does Uiedgeinsetsmake Work
Icon Already Includes Gloss Effects
App Crashes Only on Testflight Build
Prevent Duplicate Symbols When Building Static Library with Cocoapods
How to Set the Title of a Navigation Bar Programmatically
Ios11 Wkwebview Crash Due to Nsinvalidunarchiveoperationexception
Custom Tabbar Layout for Uitabbarviewcontroller
Using Tesseract to Recognize License Plates
Frosted Glass (iOS 7 Blur) Effect
Why Does Uiviewcontroller Extend Under Uinavigationbar, While Uitableviewcontroller Doesn'T
Swift - Segmented Control - Switch Multiple Views
How to Open Location Services Screen from Setting Screen