ios - example - AFNetworking: maneja el error globalmente y repite la solicitud
alamofire last version (6)
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 servidor devuelve un código de estado específico para cualquier solicitud, quiero:
- eliminar un token de autenticación en caché
- volver a autenticarse (que es una solicitud por separado)
- repite la solicitud fallida
Pensé que esto podría hacerse a través de algún controlador global de finalización / error en AFHTTPClient
, pero no encontré nada útil. Entonces, ¿cuál es la forma "correcta" de hacer lo que quiero? Anular enqueueHTTPRequestOperation:
en mi subclase AFHTTPClient
, copie la operación y ajuste el controlador de finalización original con un bloque que haga lo que quiero (volver a autenticar, poner en cola la operación copiada). ¿O estoy en el camino equivocado por completo?
¡Gracias!
EDIT: se eliminó la referencia al código de estado 401, ya que probablemente esté reservado para HTTP básico mientras estoy usando token auth.
Aquí está la implementación Swift de la answer del usuario @adamup
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
}}
donde refreshToken (...) es un método de extensión que escribí para obtener un nuevo token del servidor.
En el método init de AFHTTPClient, regístrese para AFNetworkingOperationDidFinishNotification
que se publicará después de que finalice una solicitud.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(HTTPOperationDidFinish:) name:AFNetworkingOperationDidFinishNotification object:nil];
En el controlador de notificaciones, compruebe el código de estado y copy
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 solo manejar la autenticación con este método AFNetworking:
- (void)setAuthenticationChallengeBlock:(void (^)(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge))block;
Para garantizar que no se envíen actualizaciones de tokens múltiples al mismo tiempo, es beneficioso poner en cola sus solicitudes de red y bloquear la cola cuando el token se está renovando, o agregar un bloqueo mutex (@synchronized directive) al método de actualización de token.
Si está subclasificando AFHTTPSessionManager
o usando directamente un AFURLSessionManager
, puede usar el siguiente método para establecer un bloque ejecutado después de la finalización de 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 occurred in the process of executing the task.
*/
- (void)setTaskDidCompleteBlock:(void (^)(NSURLSession *session, NSURLSessionTask *task, NSError *error))block;
Simplemente realice lo que quiera para cada tarea de la sesión:
[self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
if (httpResponse.statusCode == 500) {
}
}
}];
EDITAR: De hecho, si necesita manejar un error devuelto en el objeto de respuesta, el método anterior no hará el trabajo. Una forma si está subclases AFHTTPSessionManager
podría ser crear una subclase y establecer un serializador de respuesta personalizado 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 tu subclase AFHTTPSessionManager
:
@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
Tomó un enfoque similar, pero no pude obtener el objeto de código de estado con la respuesta de phix23, así 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");
}
}
Utilizo un medio alternativo para hacer esto con AFNetworking 2.0.
Puede subclase dataTaskWithRequest:success:failure:
y ajuste el bloque de finalización pasado con alguna comprobación de errores. Por ejemplo, si está trabajando con OAuth, podría estar atento a un error 401 (vencimiento) 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 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;
}