AFNetworking: Handle error globally and repeat request

15,792

Solution 1

In the AFHTTPClient's init method register for the AFNetworkingOperationDidFinishNotification which will be posted after a request finishes.

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(HTTPOperationDidFinish:) name:AFNetworkingOperationDidFinishNotification object:nil];

In the notification handler check the status code and copy the AFHTTPRequestOperation or create a new one.

- (void)HTTPOperationDidFinish:(NSNotification *)notification {
  AFHTTPRequestOperation *operation = (AFHTTPRequestOperation *)[notification object];

    if (![operation isKindOfClass:[AFHTTPRequestOperation class]]) {
        return;
    }

    if ([operation.response statusCode] == 401) {
        // enqueue a new request operation here
    }
}

EDIT:

In general you should not need to do that and just handle the authentication with this AFNetworking method:

- (void)setAuthenticationChallengeBlock:(void (^)(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge))block;

Solution 2

I use an alternative means for doing this with AFNetworking 2.0.

You can subclass dataTaskWithRequest:success:failure: and wrap the passed completion block with some error checking. For example, if you're working with OAuth, you could watch for a 401 error (expiry) and refresh your access token.

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)urlRequest completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))originalCompletionHandler{

    //create a completion block that wraps the original
    void (^authFailBlock)(NSURLResponse *response, id responseObject, NSError *error) = ^(NSURLResponse *response, id responseObject, NSError *error)
    {
        NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
        if([httpResponse statusCode] == 401){
            NSLog(@"401 auth error!");
            //since there was an error, call you refresh method and then redo the original task
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{

                //call your method for refreshing OAuth tokens.  This is an example:
                [self refreshAccessToken:^(id responseObject) {

                    NSLog(@"response was %@", responseObject);
                    //store your new token

                    //now, queue up and execute the original task               
                    NSURLSessionDataTask *originalTask = [super dataTaskWithRequest:urlRequest completionHandler:originalCompletionHandler];
                    [originalTask resume];
                }];                    
            });
        }else{
            NSLog(@"no auth error");
            originalCompletionHandler(response, responseObject, error);
        }
    };

    NSURLSessionDataTask *task = [super dataTaskWithRequest:urlRequest completionHandler:authFailBlock];

    return task;

}

Solution 3

Here is the Swift implementation of user @adamup 's answer

class SessionManager:AFHTTPSessionManager{
static let sharedInstance = SessionManager()
override func dataTaskWithRequest(request: NSURLRequest!, completionHandler: ((NSURLResponse!, AnyObject!, NSError!) -> Void)!) -> NSURLSessionDataTask! {

    var authFailBlock : (response:NSURLResponse!, responseObject:AnyObject!, error:NSError!) -> Void = {(response:NSURLResponse!, responseObject:AnyObject!, error:NSError!) -> Void in

        var httpResponse = response as! NSHTTPURLResponse

        if httpResponse.statusCode == 401 {
            //println("auth failed")

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), { () -> Void in

                self.refreshToken(){ token -> Void in
                    if let tkn = token{
                        var mutableRequest = request.mutableCopy() as! NSMutableURLRequest
                        mutableRequest.setValue(tkn, forHTTPHeaderField: "Authorization")
                        var newRequest = mutableRequest.copy() as! NSURLRequest
                        var originalTask = super.dataTaskWithRequest(newRequest, completionHandler: completionHandler)
                        originalTask.resume()
                    }else{
                        completionHandler(response,responseObject,error)
                    }

                }
            })
        }
        else{
            //println("no auth error")
            completionHandler(response,responseObject,error)
        }
    }
    var task = super.dataTaskWithRequest(request, completionHandler:authFailBlock )

    return task
}}

where refreshToken (...) is an extension method I wrote to get a new token from the server.

Solution 4

Took a similar approach, but I couldn't get the status code object with phix23's answer so I needed a different plan of action. AFNetworking 2.0 changed a couple of things.

-(void)networkRequestDidFinish: (NSNotification *) notification
{
    NSError *error = [notification.userInfo objectForKey:AFNetworkingTaskDidCompleteErrorKey];
    NSHTTPURLResponse *httpResponse = error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey];
    if (httpResponse.statusCode == 401){
        NSLog(@"Error was 401");
    }
}
Share:
15,792
Daniel Rinser
Author by

Daniel Rinser

iOS developer, freelancer, creator of Milk for Us.

Updated on June 22, 2022

Comments

  • Daniel Rinser
    Daniel Rinser about 2 years

    I have a use case that should be rather common but I can't find an easy way to handle it with AFNetworking:

    Whenever the server returns a specific status code for any request, I want to:

    • remove a cached authentication token
    • re-authenticate (which is a separate request)
    • repeat the failed request.

    I thought that this could be done via some global completion/error handler in AFHTTPClient, but I didn't find anything useful. So, what's the "right" way to do what I want? Override enqueueHTTPRequestOperation: in my AFHTTPClient subclass, copy the operation and wrap the original completion handler with a block that does what I want (re-authenticate, enqueue copied operation)? Or am I on the wrong track altogether?

    Thanks!

    EDIT: Removed reference to 401 status code, since that's probably reserved for HTTP basic while I'm using token auth.

  • Daniel Rinser
    Daniel Rinser over 11 years
    I haven't thought of notifications - much better and less intrusive than customizing enqueueing of requests. Thanks a lot! As to the authentication challenge block: I'm actually using token authentication rather than basic auth, so I guess that won't work, right? Sorry for having you misled by mentioning 401. Bonus question: What would be the correct response code for "invalid token"? 400?
  • Felix
    Felix over 11 years
    I'm not sure what the correct response code for "invalid token" is. Maybe 403 is more appropriate.
  • Daniel Rinser
    Daniel Rinser over 11 years
    AFAIK 403 is more for failed authorization rather than authentication ("authentication succeeded (if any), but you are not permitted to do this"). But nevermind, that's another question. Thanks again for your help.
  • Daniel Rinser
    Daniel Rinser over 11 years
    re 403: One could also argue that not providing a token or providing an invalid token causes me to be "anonymous". Thus accessing some protected resource results in an authorization error (403). Will think about it. ;)
  • Daniel Rinser
    Daniel Rinser over 11 years
    FYI (for everyone else wondering if you should copy an AF*Operation to re-execute it): "-copy and -copyWithZone: return a new operation with the NSURLRequest of the original. So rather than an exact copy of the operation at that particular instant, the copying mechanism returns a completely new instance, which can be useful for retrying operations."
  • Daniel Rinser
    Daniel Rinser over 11 years
    It seems that my optimism regarding copying the operation was wrong: First, copy copies too much stuff (eg. the response). Second, it does not include the original request's completion blocks which is a deal-breaker for me. See my follow-up question: stackoverflow.com/questions/12951037/…
  • Eli Burke
    Eli Burke almost 11 years
    DOH! Your followup comment was hidden behind "Show 1 more comment" so I merrily implemented a solution using copied blocks and then spent an hour debugging. Irritatingly, the PROGRESS block was copied and was executed (leading me to believe that everything was working), but the completion/failure block was not, as you discovered.
  • Rajan Balana
    Rajan Balana over 9 years
    Amazing implementation and approach. Thank you.
  • Trianna Brannon
    Trianna Brannon about 9 years
    Are you able to just put this in the appDelegate or does it need to go into the viewcontroller you called the request in?
  • Bruno Tereso
    Bruno Tereso about 9 years
    @TriannBrannon, you used it to complement the selected answer. Instead of using @selector(HTTPOperationDidFinish:) you use @selector(networkRequestDidFinish:) and instead of AFNetworkingOperationDidFinishNotification you use AFNetworkingTaskDidCompleteNotification. That's how I got it working
  • horseshoe7
    horseshoe7 over 7 years
    Do you know how this is accomplished with Alamofire?
  • Utsav Dusad
    Utsav Dusad over 7 years
    can you help with the access token cycle implementation with AFNetworking. We do not want to add authorization header to every request. There must be a neat way to refresh the token (on receiving 401 error) and make the request again.