AFNetworking globally prompt user to login on 401 error code? - ios

I have been struggling to find a clean solution for this problem for a few I have created an app which makes multiple restful web service requests which work fine however part of the request the login details or API Key could expire and I need to be able to handle this and present the user the login screen again.
In my API Client class I am doing the following which works fine, however because the app does multiple web service requests I am seeing the UI AlertView multiple times.
Any ideas on how I can make this block of code only run once for the first error which occurs and only show one alert view?
AFJSONRequestOperation *operation = [[AFJSONRequestOperation alloc] initWithRequest:apiRequest];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
completionBlock(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSInteger statusCode = [operation.response statusCode];
if (statusCode == 401) {
[UIAlertView error:#"Your session has expied, please log in again."];
[[NSNotificationCenter defaultCenter]
postNotificationName:#"Logout"
object:self];
} else {
completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription] forKey:#"error"]);
}
}];

One way would be to create a global variable which contains the current login status. You should check this login status before a request or before the success/failure blocks if the requests are not chained together.
A better approach would be to create a NSOperationQueue to manage the AFJSONRequestOperation objects. This would give you more control over the lifespan of each request. If one returns a 401 then you could cancel all the operations in the queue.
You can find more about creating and using queue here at this link.

Typically you encounter a similar issue when initialising the shared instance of a singleton object that you want to avoid performing the initialisation more than once.
One way to solve this is using Grand Central Dispatch's dispatch_once, as shown below, which is also included in Xcode as a default snippet (GCD: Dispatch Once). In your case you'd present the alert inside the block you pass to dispatch_once.
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/*code to be executed once*/
});

Related

Syncing new data with Contentful - iOS

I am trying to use the syncing feature of contentful so I will only fetch new or updated content form my space. From the documentation it says with the fetch I need a parameter of "initial"="true", which I am trying below, but I get 400 error. Has someone used this before? thanks
[self.client fetchEntriesMatching:#{#"content_type": #"navigationPage",
#"locale":countryCode,
#"initial":#"true"
}
success:^(CDAResponse *response, CDAArray *array) {
} failure:^(CDAResponse *response, NSError *error) {
}];
`
You're calling the entirely wrong method. You should be calling the initialSynchronizationWithSuccess:failure:which is documented here http://cocoadocs.org/docsets/ContentfulDeliveryAPI/1.10.4/Classes/CDAClient.html#//api/name/initialSynchronizationWithSuccess:failure:
That will in turn call the sync endpoint of the Contentful API with the initial parameter set to true. It will return a CDASyncedSpace object that can be used for further requests. Documented here http://cocoadocs.org/docsets/ContentfulDeliveryAPI/1.10.4/Classes/CDASyncedSpace.html

Best solution to refresh token automatically with AFNetworking?

Once your user is logged in, you get a token (digest or oauth) which you set into your HTTP Authorization header and which gives you the authorization to access your web service.
If you store your user's name, password and this token somewhere on the phone (in user defaults, or preferably in the keychain), then your the user is automatically logged in each time the application restarts.
But what if your token expires? Then you "simply" need to ask for a new token and if the user did not change his password, then he should be logged in once again automatically.
One way to implement this token refreshing operation is to subclass AFHTTPRequestOperation and take care of 401 Unauthorized HTTP Status code in order to ask a new token. When the new token is issued, you can call once again the failed operation which should now succeeds.
Then you must register this class so that each AFNetworking request (getPath, postPath, ...) now uses this class.
[httpClient registerHTTPOperationClass:[RetryRequestOperation class]]
Here is an exemple of such a class:
static NSInteger const kHTTPStatusCodeUnauthorized = 401;
#interface RetryRequestOperation ()
#property (nonatomic, assign) BOOL isRetrying;
#end
#implementation RetryRequestOperation
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *, id))success
failure:(void (^)(AFHTTPRequestOperation *, NSError *))failure
{
__unsafe_unretained RetryRequestOperation *weakSelf = self;
[super setCompletionBlockWithSuccess:success failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// In case of a 401 error, an authentification with email/password is tried just once to renew the token.
// If it succeeds, then the opration is sent again.
// If it fails, then the failure operation block is called.
if(([operation.response statusCode] == kHTTPStatusCodeUnauthorized)
&& ![weakSelf isAuthenticateURL:operation.request.URL]
&& !weakSelf.isRetrying)
{
NSString *email;
NSString *password;
email = [SessionManager currentUserEmail];
password = [SessionManager currentUserPassword];
// Trying to authenticate again before relaunching unauthorized request.
[ServiceManager authenticateWithEmail:email password:password completion:^(NSError *logError) {
if (logError == nil) {
RetryRequestOperation *retryOperation;
// We are now authenticated again, the same request can be launched again.
retryOperation = [operation copy];
// Tell this is a retry. This ensures not to retry indefinitely if there is still an unauthorized error.
retryOperation.isRetrying = YES;
[retryOperation setCompletionBlockWithSuccess:success failure:failure];
// Enqueue the operation.
[ServiceManager enqueueObjectRequestOperation:retryOperation];
}
else
{
failure(operation, logError);
if([self httpCodeFromError:logError] == kHTTPStatusCodeUnauthorized)
{
// The authentication returns also an unauthorized error, user really seems not to be authorized anymore.
// Maybe his password has changed?
// Then user is definitely logged out to be redirected to the login view.
[SessionManager logout];
}
}
}];
}
else
{
failure(operation, error);
}
}];
}
- (BOOL)isAuthenticateURL:(NSURL *)url
{
// The path depends on your implementation, can be "auth", "oauth/token", ...
return [url.path hasSuffix:kAuthenticatePath];
}
- (NSInteger)httpCodeFromError:(NSError *)error
{
// How you get the HTTP status code depends on your implementation.
return error.userInfo[kHTTPStatusCodeKey];
}
Please, be aware that this code does not work as is, as it relies on external code that depends on your web API, the kind of authorization (digest, oath, ...) and also which kind of framework you use over AFNetworking (RestKit for example).
This is quite efficient and has proved to work well with both digest and oauth authorization using RestKit tied to CoreData (in that case RetryRequestOperation is a subclass of RKManagedObjectRequestOperation).
My question now is: is this the best way to refresh a token?
I am actually wondering if NSURLAuthenticationChallenge could be used to solve this situation in a more elegant manner.
Your current solution works and you have the code for it, there might be a reasonable amount of code to achieve it but the approach has merits.
Using an NSURLAuthenticationChallenge based approach means subclassing at a different level and augmenting each created operation with setWillSendRequestForAuthenticationChallengeBlock:. In general this would be a better approach as a single operation would be used to perform the whole operation rather than having to copy it and update details, and the operation auth support would be doing the auth task instead of the operation completion handler. This should be less code to maintain, but that code will likely be understood by less people (or take longer to understand by most) so the maintenance side of things probably balances out over all.
If you're looking for elegance, then consider changing, but given that you already have a working solution there is little gain otherwise.
I was searching an answer for this problem and "Matt", the creator of AFNetworking, suggest this:
the best solution I've found for dealing with this is to use dependent
NSOperations to check for a valid, un-expired token before any
outgoing request is allowed to go through. At that point, it's up to
the developer to determine the best course of action for refreshing
the token, or acquiring a new one in the first place.

AFNetworking with Unreliable wifi - Detecting / Retrying connections when they timeout

I'm using AFNetworking in my app to connect / download data from a web service. This app is deployed via Enterprise deployment to users at various locations. At one of the locations where people use our app, the wifi network seems to randomly go down / come back up in a few seconds. In these cases, the requirement was to retry a request thrice before giving up and failing. I've got the retry part of it working fine, but have some problems detecting a network failure. Some code:
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:
^(AFHTTPRequestOperation *operation, id responseObject) {
[self parseResponse:operation.responseString];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self messageFailureWithCode:error.code
reason:[NSString stringWithFormat:#"%#", error]];
}
];
The error code I'm retrying on is NSURLErrorTimedOut. But I just got another log file which indicated a failure with the following error: -1003 "A server with the specified hostname could not be found."
I could add this code to my list too, but I wanted to be sure that I'm capturing all the errors and not just handling them as they appear. I was looking through the NSURLError.h file and found the following error codes that vaguely look like they could be caused by a network failure.
Can someone help me figuring out under what conditions each error is triggered and if I'm missing any error codes? The list is below:
NSURLErrorCannotFindHost
NSURLErrorTimedOut
NSURLErrorCannotConnectToHost
NSURLErrorNetworkConnectionLost
NSURLErrorDNSLookupFailed
NSURLErrorResourceUnavailable
NSURLErrorNotConnectedToInternet
Thanks,
Teja.
It sounds like you could safely just retry every failure with an error domain of NSURLErrorDomain (exempting the case where an operation was cancelled). That should cover all of those cases.

Issues with ACAccountStore requestAccessToAccountsWithType not calling completion block

I've been having this issue and can't find anything on it. I'm using the Social framework to build a basic Twitter application (a learning project for myself) and haven't run into any major roadblocks until just recently when the account store stopped completing my requests for account access. From all the samples I've read and gathered, when the application starts I request access to the accounts and then once granted can go from there.
The code I have to fetch the accounts is:
ACAccountType* accountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
NSLog(#"Making this call to getTwitterAccount");
[self.accountStore
requestAccessToAccountsWithType:accountType
options:nil
completion:^(BOOL granted, NSError* error) {
NSLog(#"Testing, this should show up");
if (granted) {
NSArray* accounts = [self.accountStore accountsWithAccountType:accountType];
[self getAccountFromArray:accounts];
}
else {
if (error.code == 6)
NSLog(#"No twitter account");
else
NSLog(#"Access denied");
}
}];
The two NSLog statements are in there for debugging purposes. I call the function that contains this code from viewDidLoad and the getAccountFromArray:(NSArray*) function simply determines if which account the user wishes to use for the application (presenting a modal view if necessary). My issue is with the completion block never being called unless I make a change to the accounts in the simulator. Immediately following a change to the accounts and the code executes and displays the modal (which for now just says a message) but for any other execution this block is never fired and the only print seen in the console is "Making this call to getTwitterAccount". Previously, this worked fine for fetching a single account and I was able to get the timeline fetch working and printing but then it stopped firing for some reason and I haven't been able to make a success SLRequest or do any other actions based on it since.
I've Googled and found problems with using this function out there but none of those issues are with the completion block never firing.
EDIT
Okay so an update, been working at this and for some reason moving the call to the function that checks accounts into the views initWithNibName:bundle: method seemed to work, it executes reliably (albeit a tad after the app loads). The function is called twice but only executes the completion once and only when I call it from the init - maybe a reason for this?

How to tell if TWRequest performRequestWithHandler failed or succeeded

I am posting a request and checking for errors like so:
// Send request out on a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[postRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if ([urlResponse statusCode] == 200) {
NSLog(#"Tweet Successful");
}else {
NSLog(#"Tweet Failed");
if (responseData) {
// Use the NSJSONSerialization class to parse the returned JSON
NSError *jsonError;
NSArray *requestResponse =
[NSJSONSerialization JSONObjectWithData:responseData
options:NSJSONReadingMutableLeaves
error:&jsonError];
if (requestResponse) {
NSLog(#"%#",requestResponse);
} else {
NSLog(#"%#",jsonError);
}
}
}
}];
});
It seems to work fine, so far as the request is concerned. My intention was to queue the request on failure and try again at a later date, or not depending on the error and how many attempts had failed thus far.
The problem I have is that I find it frequently appears to fail, with error 34 "Page not Found" where in fact the request did succeed, was posted as intended and can be seen clear as day in twitter stream.
Now, if I can't trust the error code returned then I can't really go on and keep retrying. How do I know for sure if it succeeds or fails?
RECENT OBSERVATIONS:
Recently, I tried to post a photo from Apple photo app directly and it sent me an alert that read "Tweet may not have been successful". That is funny, because that is basically what I was coming to as my only option in this case. This makes me think that even Apple had to acknowledge that there is not way to tell for sure that post failed when the return does not confirm it.
according to every example found in researching this problem, none of which use any other API than what you are using, and including this example from twitter's own documentation for posting a photo to the API, none of them check the urlResponse code first.
the most that ever seems to be done is this example from twitter's documentation where a GET is performed and then the response is processed. notice that it checks the responseData first, and if it exists, it is simply treated like success. only if there is no responseData is there an attempt to bother looking at the error. the example does not bother with the urlResponse … and neither does any other example i saw in 10 minutes of googling.
(finally, i doubt this matters, or it may be because you cleaned up the example for the display, but you're processing the error on the main-queue when you are not performing any UI. you could do the processing in the handler immediately, and pass along whatever post-processing you do to whatever UI you are trying to display it with. i doubt post-processing the response in the main-queue as opposed to the queue of the handler (as shown in both examples cited here and all other examples i've seen) is really causing a problem, but i wonder if it might cut down on the incidence of false negatives you are seeing. at the very least, it will make your response and any UI display of the response cleaner and more efficient.)

Resources