web-dev-qa-db-fra.com

AFNetworking: Gère l'erreur globalement et répète la demande

J'ai un cas d'utilisation qui devrait être assez courant mais je ne trouve pas de moyen facile de le gérer avec AFNetworking:

Chaque fois que le serveur renvoie un code d'état spécifique pour any request, je souhaite:

  • supprimer un jeton d'authentification mis en cache
  • ré-authentifier (ce qui est une demande séparée)
  • répétez la demande échouée.

Je pensais que cela pourrait être fait via un gestionnaire d'achèvement/d'erreur global dans AFHTTPClient, mais je n'ai rien trouvé d'utile. Alors, quelle est la "bonne" façon de faire ce que je veux? Remplacez enqueueHTTPRequestOperation: dans ma sous-classe AFHTTPClient, copiez l'opération et encapsulez le gestionnaire d'achèvement d'origine avec un bloc qui fait ce que je veux (réauthentifier, mettre en file d'attente l'opération copiée)? Ou suis-je sur la mauvaise voie?

Merci!

EDIT: Suppression de la référence au code d'état 401, car elle est probablement réservée à HTTP basic lorsque j'utilise l'authentification par jeton.

36
Daniel Rinser

Dans la méthode init de AFHTTPClient, enregistrez pour la AFNetworkingOperationDidFinishNotification qui sera publiée à la fin de la demande.

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

Dans le gestionnaire de notifications, vérifiez le code d'état et copy la AFHTTPRequestOperation ou créez-en un nouveau.

- (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
    }
}

MODIFIER:

En général, vous ne devriez pas avoir à faire cela et simplement gérer l'authentification avec cette méthode AFNetworking:

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

J'utilise un autre moyen de le faire avec AFNetworking 2.0. 

Vous pouvez sous-classer dataTaskWithRequest:success:failure: et envelopper le bloc d'achèvement passé avec une vérification d'erreur. Par exemple, si vous travaillez avec OAuth, vous pouvez rechercher une erreur 401 (expiration) et actualiser votre jeton d'accès.

- (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;

}
28
adamup

Voici l'implémentation Swift de l'utilisateur @adamup ' 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
}}

où refreshToken (...) est une méthode d'extension que j'ai écrite pour obtenir un nouveau jeton du serveur.

4
darthjit

J'ai adopté une approche similaire, mais je ne pouvais pas obtenir l'objet code d'état avec la réponse de phix23, donc j'avais besoin d'un plan d'action différent. AFNetworking 2.0 a changé plusieurs choses.

-(void)networkRequestDidFinish: (NSNotification *) notification
{
    NSError *error = [notification.userInfo objectForKey:AFNetworkingTaskDidCompleteErrorKey];
    NSHTTPURLResponse *httpResponse = error.userInfo[AFNetworkingOperationFailingURLResponseErrorKey];
    if (httpResponse.statusCode == 401){
        NSLog(@"Error was 401");
    }
}
2
drees

Si vous sous-classez AFHTTPSessionManager ou utilisez directement un AFURLSessionManager, vous pouvez utiliser la méthode suivante pour définir un bloc exécuté après l'achèvement d'une tâche:

/**
 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;

Effectuez simplement ce que vous voulez faire pour chacune des tâches de la session:

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

        }
     }
}];

EDIT: En fait, si vous devez traiter une erreur renvoyée dans l'objet de réponse, la méthode ci-dessus ne fera pas le travail . Une façon de sous-classer AFHTTPSessionManager pourrait être de sous-classe et définissez un sérialiseur de réponse personnalisé avec responseObjectForResponse:data:error: surchargé de la manière suivante:

@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

et définissez-le dans votre AFHTTPSessionManager sous-classe:

@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
1
Bluezen

Pour vous assurer que plusieurs actualisations de jetons ne sont pas émises à peu près au même moment, il est utile de mettre vos demandes réseau en file d'attente et de la bloquer lorsque le jeton est actualisé ou d'ajouter un verrou mutex (directive @synchronized) à votre méthode d'actualisation de jetons.

0
Ryan