I am currently working on a POC app, I have previously posted about it here. I am trying to handle automatic refreshing of an authentication token should my server give me a 401 error (unauthorised).
Here is my demo function that requests some information from the server (I can deliberately send it valid/invalid auth tokens)
NSInteger retryAttempts = 0;
NSInteger retryMax = 1;
- (void) requestDataForUser {
NSLog(#"requestDataForUser - Called");
//Indicate Network Activity
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = TRUE;
});
//Build request URL String
NSString *requestString = [NSString stringWithFormat:#"%#%#%#",baseURL,requestURL,#"3"];//Change to allow change in username here.
//Get auth token
NSString *accessToken = [SAMKeychain passwordForService:kServer account:kKeyAccessToken];
NSString *requestAuthorization = [NSString stringWithFormat:#"%# %#", #"Bearer", accessToken];
//Initialize url request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
//Set the url for the request
[request setURL:[NSURL URLWithString:requestString]];
//Set HTTP method for request
[request setHTTPMethod:#"GET"];
//Set HTTP header field with the authorization token
[request setValue:requestAuthorization forHTTPHeaderField:#"Authorization"];
//Create full request
NSURLSession *session = [NSURLSession sharedSession];
__weak typeof (self) weakSelf = self;
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
NSLog(#"Status Code: %ld\n",(long)httpResponse.statusCode);
NSString *message = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
NSLog(#"Message: %#", message);
NSLog(#"requestDataForUser - Responce from server");
//Check for an error, if there is no error we proceed.
if (!error) {
if (retryAttempts <= retryMax) {
switch (httpResponse.statusCode) {
case 200 ... 299:
NSLog(#"SUCCESS");
NSLog(#"Performing any completion related functions!");
break;
case 401:
NSLog(#"401 Challenge - Retrying Authentication, Attempt %ld", (long)retryAttempts);
[weakSelf refreshAuth];
[weakSelf requestDataForUser];//retries this function
retryAttempts += 1;
break;
}}
else {
NSLog(#"401 Error Recieved - Retried credentials %ld time(s), please check your details are correct", (long)retryMax);
retryAttempts = 0; //Reset retry counter
//Alert controller?
}
//Get que and perform any UI changes
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = FALSE;
});
}
else {
//Failed request
NSLog(#"requestDataForUser - error : %#", error.description);
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = FALSE;
});
}
}];
[dataTask resume];
}
The problems I am having with this come in the 401 challenge section of the request. What I want to do is request/refresh a new token (refresh in the final iteration but currently my server is a bit hit/miss on token refreshes so I am requesting a new token in this example). So lets look at my server challenge section:
case 401:
NSLog(#"401 Challenge - Retrying Authentication, Attempt %ld", (long)retryAttempts);
[weakSelf refreshAuth];
[weakSelf requestDataForUser];//retries this function
retryAttempts += 1;
break;
So i am printing out the attempt number here, I can manually set the amount of times that this 'block' is retried until it gives up and throws an error at the user. Next it will call for an auth token, retry the request and increase retryAttempts by 1.
My problem is that when I request a new token I'm doing it asynchronously so the request is sent off and then my function retries itself (obviously without a new token) and then it throws the error. And then my token returns and prints to the console that a new token returned successfully.
I have had a look at semaphores but I can't seem to get them to work (as my requestAuthToken method has no completion block). Is there anyway I can force the auth request to be syncronous?
I have also tried to get my requestAuth method to return a BOOL and loop the bool within the 401 block until it becomes true, however it never gets set to true and the while loop goes on forever.
Any and all help is appreciated!
Assuming you can change implementation of requestAuth, add a completion handler parameter to requestAuth function.
Inside requestAuth implementation, call that handler after token is received. Then in requestDataForUser:
case 401:
NSLog(#"401 Challenge - Retrying Authentication, Attempt %ld", (long)retryAttempts);
[weakSelf refreshAuth withCompletionHandler:weakself.requestDataForUser];
retryAttempts += 1;
break;
Otherwise, use NSOperationQueue and set maximum concurrent operation to 1:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
[queue addOperationWithBlock:^{
[weakself requestAuth];
}];
[queue addOperationWithBlock:^{
[weakself requestDataForUser];
}];
Related
In my app, I need to add my authentication token in the HTTPHeadField for every NSURLRequest API call that I make to my server. This token is only valid for 2 days. When it becomes invalid, I'll receive a "token_invalid" error response from my server, meaning that I'll need to send an API call to my server to refresh my auth token.
The problem that's hard to wrap my head around is that these NSURLRequests are done concurrently, so when each fails due to an expired token, ALL of them are going to attempt to refresh the token. How do I set this up so that the token is refreshed ONCE, and when that's done, re-attempt all the failed requests?
PROGRESS
What I have so far works, but only to a certain extent that confuses me. When I successfully refresh the auth token, I iterate through all the failed requests, and re-attempt them. However, all of them are being re-attempted in that ONE API call that was responsible for refreshing the auth token.
For example, 3 API calls are being made (Friend Requests, Notifications, and Getting a User's Friends). If the "Get Friend Requests" API call fails first, it's responsible for refreshing the token. The other two API requests are put in the failedRequests array. When the auth token is successfully refreshed, only the "Get Friend Request" API call's success block is being passed through...3 TIMES!
I kinda understand why it's doing that, because I'm re-attempting all the failed API requests in the context of one NSURLRequest's sendTask method. Is there a way for me to re-attempt the failed requests in their given contexts when the auth token is refreshed in the kind of way that Key-Value Observing works?
-(void)sendTask:(NSURLRequest*)request successCallback:(void (^)(NSDictionary*))success errorCallback:(void (^)(NSString*))errorCallback
{
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
MyAPIInterface *__weak weakSelf = self;
[self parseResponse:response data:data fromRequest:request successCallback:success errorCallback:^(NSString *error)
{
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 401) {
if ([error isEqualToString:#"invalid_credentials"]) {
errorCallback(#"Invalid username and/or password");
}
else if ([error isEqualToString:#"token_expired"]) {
// check if request's auth token differs from api's current auth token
NSArray *requestHeaderValueComponents = [[request valueForHTTPHeaderField:#"Authorization"] componentsSeparatedByString:#" "];
NSString *requestAuthToken = requestHeaderValueComponents[1];
// if new auth token hasn't been retrieved yet
if ([requestAuthToken isEqualToString:weakSelf.authToken]) {
NSLog(#"THE AUTH TOKENS ARE EQUAL");
if (!weakSelf.currentlyRefreshingToken.boolValue) {
//lock alreadyRefreshingToken boolean
weakSelf.currentlyRefreshingToken = [NSNumber numberWithBool:YES];
NSLog(#"NOT REFRESHING TOKEN");
// add mutable failed request (to change auth token header later) to failedRequests array
NSMutableArray *mutableFailedRequests = [weakSelf.failedRequests mutableCopy];
NSMutableURLRequest *mutableFailedRequest = [request mutableCopy];
[mutableFailedRequests addObject:mutableFailedRequest];
weakSelf.failedRequests = [mutableFailedRequests copy];
// refresh auth token
[weakSelf refreshAuthenticationTokenWithSuccessCallback:^(NSDictionary *response) {
//store authToken
weakSelf.authToken = response[#"token"];
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:weakSelf.authToken forKey:#"authToken"];
[defaults synchronize];
//change auth token http header of each failed request and re-attempt them
for (NSMutableURLRequest *failedRequest in weakSelf.failedRequests) {
NSString *newAuthHeaderValue = [NSString stringWithFormat:#"Bearer %#", weakSelf.authToken];
[failedRequest setValue:newAuthHeaderValue forHTTPHeaderField:#"Authorization"];
[weakSelf sendTask:failedRequest successCallback:success errorCallback:errorCallback];
}
//clear failedRequests array and unlock alreadyRefreshingToken boolean
[weakSelf clearFailedRequests];
weakSelf.currentlyRefreshingToken = [NSNumber numberWithBool:NO];
NSLog(#"TOKEN REFRESHING SUCCESSFUL");
} errorCallback:^(NSString *error) {
NSLog(#"TOKEN NOT REFRESHABLE! HAVE TO LOG IN MANUALLY");
//clear failedRequests array
[weakSelf clearFailedRequests];
weakSelf.currentlyRefreshingToken = [NSNumber numberWithBool:NO];
errorCallback(#"Your login session has expired");
}];
}
else {
NSLog(#"ALREADY REFRESHING TOKEN. JUST ADD TO FAILED LIST");
// add mutable failed request (to change auth token header later) to failedRequests array
NSMutableArray *mutableFailedRequests = [weakSelf.failedRequests mutableCopy];
NSMutableURLRequest *mutableFailedRequest = [request mutableCopy];
[mutableFailedRequests addObject:mutableFailedRequest];
weakSelf.failedRequests = [mutableFailedRequests copy];
}
}
// if new auth token has been retrieved, simply re-attempt request with new auth token
else {
NSMutableURLRequest *failedRequest = [request mutableCopy];
NSString *newAuthHeaderValue = [NSString stringWithFormat:#"Bearer %#", weakSelf.authToken];
[failedRequest setValue:newAuthHeaderValue forHTTPHeaderField:#"Authorization"];
[weakSelf sendTask:failedRequest successCallback:success errorCallback:errorCallback];
}
}
else {
errorCallback(error);
}
}
else {
errorCallback(error);
}
}];
}];
[task resume];
}
1)I think you should be getting the token from successful login to the account.
2)So when ever the token gets expired. Show login screen to user.
3)If user logged in successfully he get new access token.
4) You can use this for your next request
I am sending a network request from a block in objective-c.My function is called but i don't get the response of the network call means the delegate methods are never called.I think this is the issue with thread related to it.Actually i am trying to access the contacts of user.For that purpose a block is always called.When block is called it ask user for the permission to allow access to contacts.when user press `'OK' then i am calling my custom function.Function is called but the response is not recieved & delegate are also not called.
access contacts:
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, NULL);
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined)
{
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
// First time access has been granted, add the contact
NSLog(#"access contact");
[self sample];//Here is the function i call for making network request.
});
}
else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized)
{
// The user has previously given access, add the contact
NSLog(#"previous access");
}
else
{
// The user has previously denied access
// Send an alert telling user to change privacy setting in settings app
NSLog(#"send alert");
}
Function to make network call:
-(void)sample
{
NSLog(#"sample func called");
// Create the request.
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://192.168.1.42/login"]];
// Specify that it will be a POST request
request.HTTPMethod = #"POST";
// This is how we set header fields
[request setValue:#"application/xml; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
// Convert your data and set your request's HTTPBody property
NSString *stringData = #"some data";
NSData *requestBodyData = [stringData dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPBody = requestBodyData;
// Create url connection and fire request
sampleConn= [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
use this
dispatch_async(dispatch_get_main_queue(), ^{
[self sample];
});
I'm dealing the authenticate issue with Tumblr account using [NSURLConnection sendAsynchronousRequest:queue:completionHandler:] to send the authenticate request, but here I meet a tough problem:
Whenever I send the request at the first time, everything goes perfectly, but when the first authentication is done and then resend the request second time, there comes "NSURLErrorDomain error -1012".
The authenticate page is loaded in a webview so that the authentication should be done in my app without a browser. But it is interesting that if the process runs in a browser there comes no error, errors only happen when using webview.
It was weird that the authentication goes with the same code, but only the first authentication can be done, only if I reinstall the app can I authenticate it again, and after this the problem comes again.
I did everything I can chase to solve the issue, I clean the cache and cookie in webview, step the authentication process to see parameters, set the cachePolicy of the request but nothing helps.
I also found that on ios6 the process goes without any error. But on ios7 I get the -1012.
code -1012 tells me that the user cancelled the authentication, but the process goes automatically and I do not cancel it.
I'm wondering if the problem comes from the NSURLConnection.
- (void)authenticate:(NSString *)URLScheme WithViewController:(UIViewController *)con callback:(TMAuthenticationCallback)callback {
self.threeLeggedOAuthTokenSecret = nil;
self.hostViewController = con;
self.callback = callback;
[self emptyCookieJar];
NSString *tokenRequestURLString = [NSString stringWithFormat:#"http://www.tumblr.com/oauth/request_token?oauth_callback=%#", TMURLEncode([NSString stringWithFormat:#"%#://tumblr-authorize", URLScheme])];
NSLog(#"%#", tokenRequestURLString);
NSMutableURLRequest *request = mutableRequestWithURLString(tokenRequestURLString);
NSLog(#"%#", request);
[[self class] signRequest:request withParameters:nil consumerKey:self.OAuthConsumerKey
consumerSecret:self.OAuthConsumerSecret token:nil tokenSecret:nil];
[self openOAuthViewController];
NSURLConnectionCompletionHandler handler = ^(NSURLResponse *response, NSData *data, NSError *error) {
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
if (error) {
if (callback) {
callback(nil, nil, error);
}
return;
}
NSLog(#"%d", statusCode);
if (statusCode == 200) {
self.threeLeggedOAuthCallback = callback;
NSDictionary *responseParameters = formEncodedDataToDictionary(data);
self.threeLeggedOAuthTokenSecret = responseParameters[#"oauth_token_secret"];
NSURL *authURL = [NSURL URLWithString:
[NSString stringWithFormat:#"http://www.tumblr.com/oauth/authorize?oauth_token=%#",
responseParameters[#"oauth_token"]]];
[self initOAuthViewControllerWithURL:authURL];
} else {
if (callback) {
callback(nil, nil, errorWithStatusCode(statusCode));
}
}
};
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:handler];
}
Code above, everything goes normally before [NSURLConnection sendAsynchronousRequest:queue:completionHandler:],and after this method I got the error in completionHandler.
I want to call a unspecified number of URL requests which must be fired one after other. As the server can´t handle multiple requests with identical user-ID at the same time (only the last request is processed) i have to send my requests in an interval with about 1 seconds of gap. I did that within a dispatch_after block and increasing delays. But this is neither really secure nor elegant.
I´ve been just reading all day about GCD and want to try to change my code to send URL requests in a chain. My server connection class is build upon a NSURLConnection with asynchronuous request. That means it wouldn´t work with dispatch_async as the method call returns immediately back and the next request in the dispatch queue is called (which is probably immediately). But i have to wait for the response of the server until i may send the next request. My server connection class sends back via a delegate, but with dispatch_async it is never sending any deletate callbacks. Anyhow it wouldn´t work this way.
Probably it is better to put all requests into a NSArray and then call a method which will send requests from the array to the connection class and the delegate callback will pop the item from the array and sending the next request till all requests are done. Unfortunately i absolutely have no idea how i could store the requests and parameters in an array. Currently my call looks like that:
- (void)sendSettings
{
//NSLog(#"begins: %s", __FUNCTION__);
dataProtocol = [[BackgroundSoundConnection alloc] init];
[dataProtocol setDelegate:self];
//double delayInSeconds;
//dispatch_time_t popTime;
//delayInSeconds = 0.1f;
if (self.switch1.on)
{
if (![self.pinnedSettings.nextCall.globalId isEqualToString:self.sound.globalId]) {
[dataProtocol requestDataFromServer:[NSString stringWithFormat:#"setBackgroundSoundNextCall/%#", self.sound.globalId] httpMethod:#"PUT" sound:self.sound stickerType:#"nextCall" personMSISDN:nil];
}
} else {
if ([self.pinnedSettings.nextCall.globalId isEqualToString:self.sound.globalId]) {
[dataProtocol requestDataFromServer:[NSString stringWithFormat:#"disableBackgroundSoundNextcall"] httpMethod:#"PUT" sound:nil stickerType:nil personMSISDN:nil];
}
}
if (self.switch2.on)
{
if (![self.pinnedSettings.incomingCalls.globalId isEqualToString:self.sound.globalId]) {
[dataProtocol requestDataFromServer:[NSString stringWithFormat:#"setBackgroundSoundIncoming/%#", self.sound.globalId] httpMethod:#"PUT" sound:self.sound stickerType:#"incomingCalls" personMSISDN:nil];
}
} else {
if ([self.pinnedSettings.incomingCalls.globalId isEqualToString:self.sound.globalId]) {
[dataProtocol requestDataFromServer:[NSString stringWithFormat:#"disableBackgroundSoundIncoming"] httpMethod:#"PUT" sound:nil stickerType:nil personMSISDN:nil];
}
}
if (self.switch3.on)
{
if (![self.pinnedSettings.outgoingCalls.globalId isEqualToString:self.sound.globalId]) {
[dataProtocol requestDataFromServer:[NSString stringWithFormat:#"setBackgroundSoundOutgoing/%#", self.sound.globalId] httpMethod:#"PUT" sound:self.sound stickerType:#"outgoingCalls" personMSISDN:nil];
}
} else {
if ([self.pinnedSettings.outgoingCalls.globalId isEqualToString:self.sound.globalId]) {
[dataProtocol requestDataFromServer:[NSString stringWithFormat:#"disableBackgroundSoundOutgoing"] httpMethod:#"PUT" sound:nil stickerType:nil personMSISDN:nil];
}
}
for (int i = 0; i < [personArray count]; i++)
{
if (![personArray[i] connectedToServer])
{
NSLog(#"sound: %#", [personArray[i] soundId]);
NSLog(#"msisdn: %#", [personArray[i] personMSISDN]);
[dataProtocol requestDataFromServer:[NSString stringWithFormat:#"setBackgroundSoundContext/%#/%#", [personArray[i] soundId], [personArray[i] personMSISDN]] httpMethod:#"PUT" sound:self.sound stickerType:#"contextCalls" personMSISDN:[personArray[i] personMSISDN]];
}
}
[self animateViewAway:self.view];
}
A part of the request parameters is already in an array. I could use this array and push the other request parameters into it and then sending the first parameter. And after server responded send the next request triggered by the callback from the delegate. Probably this would work.
But i´m just wondering if there isn´t andy way to que the requests a dispatch queue. But how could i que the delegates as well? Or what do i have to do that the queue will wait until the server responds? I´d like to avoid rewriting my server connection class from asynchronous to synchronous URLConnection which would probably make the difference.
Can anybody point me to a solution with asynchronous URLConnection and dispatch_async?
I haven´t seen the possibilites of NSOperation and NSOperationQueue yet. In the podcast of Jeff Kelley i´ve heard that the advantage of GCD over NSOperation is the dependencies feature. http://iphreaksshow.com/042-iphreaks-show-concurrency-with-jeff-kelley/
Or did i mix up everything? What would you recommend?
A complete NSURLRequest represents a complete request by containing a path, query params or body, headers, etc. You can build several of these to represent your several server requests.
NSURLConnection provides an asynch send (sendAsynchronousRequest:queue:completionHandler:). A naive way to sequence a series of requests, is to nest the requests in completion blocks as follows...
[NSURLConnection sendAsynchronousRequest:request0 queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
[NSURLConnection sendAsynchronousRequest:request1 queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
// and so on... yikes, we'll have code in column 1000 pretty soon
But it should be clear that this is a weak idea. You can get the same effect for sequencing an arbitrary number of requests with pretty compact code as follows:
- (void)doManyRequests:(NSArray *)requests withResults:(NSMutableArray *)results completion:(void (^)(void))completion {
if (!requests.count) {
return completion();
}
NSURLRequest *nextRequest = requests[0];
NSArray *remainingRequests = [requests subarrayWithRange:NSMakeRange(1, requests.count-1)];
[NSURLConnection sendAsynchronousRequest:nextRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
[results addObject:data];
[self doManyRequests:remainingRequests withResults:results completion:completion];
}];
}
Now, as you suggested, prepare several requests and place them in an array:
NSURLRequest *request0 = // however you build this for a given user id
NSURLRequest *request1 = // etc.
NSURLRequest *request2 = // etc.
NSArray *requests = #[request0, request1, request2];
NSMutableArray *results = [NSMutableArray array];
[self doManyRequests:requests withResults:results completion:^{
NSLog(#"this will be an array of NSData objects %#", results);
}];
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.
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;
}
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;
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.
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");
}
}
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
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.