AFNetworking: Handle error globally and repeat request

前端 未结 6 1647
温柔的废话
温柔的废话 2020-12-07 11:07

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 <

相关标签:
6条回答
  • 2020-12-07 11:53

    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;
    
    }
    
    0 讨论(0)
  • 2020-12-07 12:01

    If you are subclassing AFHTTPSessionManager or using directly an AFURLSessionManager you could use the following method to set a block executed after the completion of a task:

    /**
     Sets a block to be executed as the last message related to a specific task, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didCompleteWithError:`.
    
     @param block A block object to be executed when a session task is completed. The block has no return value, and takes three arguments: the session, the task, and any error that occurred in the process of executing the task.
    */
    - (void)setTaskDidCompleteBlock:(void (^)(NSURLSession *session, NSURLSessionTask *task, NSError *error))block;
    

    Just perform whatever you want to do for each tasks of the session in it:

    [self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
        if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) {
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
            if (httpResponse.statusCode == 500) {
    
            }
         }
    }];
    

    EDIT: In fact if you need to handle an error returned in the response object the above method won't do the job. One way if you are subclassing AFHTTPSessionManager could be to subclass and set a custom response serializer with it's responseObjectForResponse:data:error: overloaded like that:

    @interface MyJSONResponseSerializer : AFJSONResponseSerializer
    @end
    
    @implementation MyJSONResponseSerializer
    
    #pragma mark - AFURLResponseSerialization
    - (id)responseObjectForResponse:(NSURLResponse *)response
                               data:(NSData *)data
                              error:(NSError *__autoreleasing *)error
    {
        id responseObject = [super responseObjectForResponse:response data:data error:error];
    
        if ([responseObject isKindOfClass:[NSDictionary class]]
            && /* .. check for status or error fields .. */)
        {
            // Handle error globally here
        }
    
        return responseObject;
    }
    
    @end
    

    and set it in your AFHTTPSessionManager subclass:

    @interface MyAPIClient : AFHTTPSessionManager
    + (instancetype)sharedClient;
    @end
    
    @implementation MyAPIClient
    
    + (instancetype)sharedClient {
        static MyAPIClient *_sharedClient = nil;
        static dispatch_once_t onceToken;
    
        dispatch_once(&onceToken, ^{
            _sharedClient = [[MyAPIClient alloc] initWithBaseURL:[NSURL URLWithString:MyAPIBaseURLString]];
            _sharedClient.responseSerializer = [MyJSONResponseSerializer serializer];
        });
    
        return _sharedClient;
    }
    
    @end
    
    0 讨论(0)
  • 2020-12-07 12:02

    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;
    
    0 讨论(0)
  • 2020-12-07 12:02

    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.

    0 讨论(0)
  • 2020-12-07 12:02

    To ensure that multiple token refreshes are not issued at around the same time, it is beneficial to either queue your network requests and block the queue when the token is refreshing, or add a mutex lock (@synchronized directive) to your token refresh method.

    0 讨论(0)
  • 2020-12-07 12:12

    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");
        }
    }
    
    0 讨论(0)
提交回复
热议问题