AFNetworking: maneja el error globalmente y repite la request

Tengo un caso de uso que debería ser bastante común, pero no puedo encontrar una manera fácil de manejarlo con AFNetworking:

Cada vez que el server devuelve un código de estado específico para cualquier request, quiero:

  • eliminar un token de authentication en caching
  • re-authenticate (que es una request por separado)
  • Repita la request fallida.

Pensé que esto podría hacerse a través de un controller global de finalización / error en AFHTTPClient , pero no encontré nada útil. Entonces, ¿cuál es la forma "correcta" de hacer lo que quiero? Reemplace enqueueHTTPRequestOperation: en mi subclass AFHTTPClient , copie la operación y envuelva el manejador de finalización original con un bloque que haga lo que quiera (¿volver a autenticar, enqueuer la operación copyda)? ¿O estoy en el path equivocado por completo?

¡Gracias!

EDIT: Se eliminó la reference al código de estado 401, ya que probablemente se haya reservado para HTTP básico mientras estoy usando token auth.

En el logging del método init de AFHTTPClient para AFNetworkingOperationDidFinishNotification que se publicará una vez finalizada la request.

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

En el manejador de notifications, compruebe el código de estado y copy el AFHTTPRequestOperation o cree uno nuevo.

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

EDITAR:

En general, no debería necesitar hacer eso y simplemente manejar la authentication con este método AFNetworking:

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

Utilizo un medio alternativo para hacer esto con AFNetworking 2.0.

Puede subclass dataTaskWithRequest:success:failure: y envuelva el bloque de finalización pasado con alguna comprobación de errores. Por ejemplo, si está trabajando con OAuth, puede ver un error de 401 (caducidad) y actualizar su token de acceso.

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

Tomé un enfoque similar, pero no pude get el object de código de estado con la respuesta de phix23, por lo que necesitaba un plan de acción diferente. AFNetworking 2.0 cambió un par de cosas.

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

Aquí está la implementación Swift de la respuesta del usuario @adamup

 class SessionManager:AFHTTPSessionManager{ static let shanetworkingInstance = 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 }} 

donde refreshToken (…) es un método de extensión que escribí para get un nuevo token del server.

Si está subclassando AFHTTPSessionManager o usando directamente un AFURLSessionManager , puede usar el siguiente método para configurar un bloque ejecutado después de completar una tarea:

 /** 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 occurnetworking in the process of executing the task. */ - (void)setTaskDidCompleteBlock:(void (^)(NSURLSession *session, NSURLSessionTask *task, NSError *error))block; 

Simplemente realice lo que quiera hacer para cada tarea 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: De hecho, si necesita manejar un error devuelto en el object de respuesta, el método anterior no hará el trabajo. Una forma si está subclassando AFHTTPSessionManager podría ser subclass y establecer un serializador de respuesta personalizada con su responseObjectForResponse:data:error: sobrecargado así:

 @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 

y AFHTTPSessionManager en su subclass AFHTTPSessionManager :

 @interface MyAPIClient : AFHTTPSessionManager + (instancetype)shanetworkingClient; @end @implementation MyAPIClient + (instancetype)shanetworkingClient { static MyAPIClient *_shanetworkingClient = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _shanetworkingClient = [[MyAPIClient alloc] initWithBaseURL:[NSURL URLWithString:MyAPIBaseURLString]]; _shanetworkingClient.responseSerializer = [MyJSONResponseSerializer serializer]; }); return _shanetworkingClient; } @end 

Para asegurarse de que no se emitan varias actualizaciones de tokens al mismo time, es beneficioso hacer queue en las requestes de networking y bloquear la queue cuando se refresca el token, o agregar un locking mutex (directiva @synchronized) a su método de actualización de tokens.