Alamofire:How to Handle Errors Globally

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...299
  • Content-Type - This header in the response must match the Accept 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



Leave a reply



Submit