Working on an IOS9 app that is doing a background URLSession in a controller that is a NSURLSessionDelegate. Here is how I start it:
self.session_data = [[NSMutableData alloc] init];
NSURL *url = [NSURL URLWithString:src];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier: #"myBackgroundSessionIdentifier"];
self.session = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
self.download = [self.session dataTaskWithRequest: request ];
[self.download resume];
So far so good. I implement the three delegate methods. 'didReceiveData' is called first and I store the data.
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data{
NSLog(#"%s",__func__);
[self.session_data appendData:data];
}
Right after that 'didCompleteWithError' is called. The 'completionHandler' handler is never called.
What is confusing about 'didCompleteWithError' message is that the actual error object is nil. I have seen some similar unanswered questions. I am not leaving the controller/view while loading. Do I need to move that functionality into AppDelegate?
Apple doc said that didCompleteWithError report only client side error, otherwise is nil:
"Server errors are not reported through the error parameter. The only errors your delegate receives through the error parameter are client-side errors, such as being unable to resolve the hostname or connect to the host."
This is the link to the documentation.
If you want to check other errors like session's errors you have to implement session protocol delegate
- URLSession:didBecomeInvalidWithError:
For more details, see this answer
Related
iOS 8, XCode 6.3.2
I want to download multiple files serially.
In the wake of the Push notification, APP will start BackgroudDownload by NSURLSessionDownloadTask.
After the First BackgroudDownload process has been completed, APP want to start Second process, but Second BackgroudDownload process does not start.
Code is below
// This method is called by Push Notification
- (void)startBackgroundDownload
{
// Session
NSURLSessionConfiguration *configFirst = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.test.first"];
sessionFirst = [NSURLSession sessionWithConfiguration:configFirst delegate:self delegateQueue:nil];
NSURLSessionConfiguration *configSecond = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.test.second"];
sessionSecond = [NSURLSession sessionWithConfiguration:configSecond delegate:self delegateQueue:nil];
// Start First Download
NSURLRequest *requestFirst = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://xxxxx/first.zip"]];
NSURLSessionDownloadTask *downloadTaskFirst = [sessionFirst downloadTaskWithRequest:requestFirst];
[downloadTaskFirst resume];
}
// Finish Download
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
if (session == sessionFirst) {
NSURLRequest *requestSecond = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://xxxxx/second.zip"
NSURLSessionDownloadTask *downloadTaskSecond = [sessionSecond downloadTaskWithRequest:requestSecond];
[downloadTaskSecond resume];
} else if (session == sessionSecond) {
NSLog(#"all finish");
}
}
The First is successful, and the Second is fail (not start).
I want advice to pursue the cause.
Thank you for any help you can provide.
downloading task is divide in perfect part like as follow.
First make one array of zip files which you want to download.
Initialise session object
Write one method which can get URL and "startDownloading"
In delegate method (successful download) called unzip that file. remove first object of zip array and again called "startDownloading" method and its call until your array count is greater than zero
I hope you will understand what I want to explain here.
I am using NSURLSessionDownloadTask with background sessions to achieve all my REST requests. This way I can use the same code without have to think about my application being in background or in foreground.
My back-end has been dead for a while, and I have taken that opportunity to test how does NSURLSession behave with timeouts.
To my utter surprise, none of my NSURLSessionTaskDelegate callbacks ever gets called. Whatever timeout I set on the NSURLRequest or on the NSURLSessionConfiguration, I never get any callback from iOS telling me that the request did finish with timeout.
That is, when I start a NSURLSessionDownloadTask on a background session. Same behavior happens the application is in background or foreground.
Sample code:
- (void)launchDownloadTaskOnBackgroundSession {
NSString *sessionIdentifier = #"com.mydomain.myapp.mySessionIdentifier";
NSURLSessionConfiguration *backgroundSessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
backgroundSessionConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringCacheData;
backgroundSessionConfiguration.timeoutIntervalForRequest = 40;
backgroundSessionConfiguration.timeoutIntervalForResource = 65;
NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration:backgroundSessionConfiguration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://www.timeout.com/"]];
request.timeoutInterval = 30;
NSURLSessionDownloadTask *task = [backgroundSession downloadTaskWithRequest:request];
[task resume];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(#"URLSession:task:didCompleteWithError: id=%d, error=%#", task.taskIdentifier, error);
}
However, when I use the default session, then I do get an error callback after 30seconds (the timeout that I set at request level).
Sample code:
- (void)launchDownloadTaskOnDefaultSession {
NSURLSessionConfiguration *defaultSessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
defaultSessionConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringCacheData;
defaultSessionConfiguration.timeoutIntervalForRequest = 40;
defaultSessionConfiguration.timeoutIntervalForResource = 65;
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultSessionConfiguration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://www.timeout.com/"]];
request.timeoutInterval = 30;
NSURLSessionDownloadTask *task = [defaultSession downloadTaskWithRequest:request];
[task resume];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(#"URLSession:task:didCompleteWithError: id=%d, error=%#", task.taskIdentifier, error);
}
I cannot seem to find in the documentation anything that suggests that the timeout should behave differently when using background sessions.
Has anyone bumped into that issue as well?
Is that a bug or a feature?
I am considering creating a bug report, but I usually get feedback much faster on SO (a few minutes) than on the bug reporter (six months).
Regards,
Since iOS8, the NSUrlSession in background mode does not call this delegate method if the server does not respond.
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
The download/upload remains idle indefinitely.
This delegate is called on iOS7 with an error when the server does not respond.
In general, an NSURLSession background session does not fail a task if
something goes wrong on the wire. Rather, it continues looking for a
good time to run the request and retries at that time. This continues
until the resource timeout expires (that is, the value of the
timeoutIntervalForResource property in the NSURLSessionConfiguration
object you use to create the session). The current default for that
value is one week!
Quoted information taken from this Source
In other words, the behaviour of failing for a timeout in iOS7 was incorrect. In the context of a background session, it is more interesting to not fail immediately because of network problems. So since iOS8, NSURLSession task continues even if it encounters timeouts and network loss. It continues however until timeoutIntervalForResource is reached.
So basically timeoutIntervalForRequest won't work in Background session but timeoutIntervalForResource will.
Timeout for DownloadTask is thrown by NSURLSessionTaskDelegate not NSURLSessionDownloadDelegate
To trigger a timeout(-1001) during a downloadTask:
Wait till download starts.
percentage chunks of data downloading will trigger:
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
Then PAUSE the whole app in XCode debugger.
Wait 30secs.
Unpause the app using XCode debugger buttons
The http connection from server should time out and trigger:
-1001 "The request timed out."
#pragma mark -
#pragma mark NSURLSessionTaskDelegate - timeouts caught here not in DownloadTask delegates
#pragma mark -
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
if(error){
ErrorLog(#"ERROR: [%s] error:%#", __PRETTY_FUNCTION__,error);
//-----------------------------------------------------------------------------------
//-1001 "The request timed out."
// ERROR: [-[SNWebServicesManager URLSession:task:didCompleteWithError:]] error:Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSUnderlyingError=0x1247c42e0 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, NSErrorFailingURLStringKey=https://directory.clarksons.com/api/1/dataexport/ios/?lastUpdatedDate=01012014000000, NSErrorFailingURLKey=https://directory.clarksons.com/api/1/dataexport/ios/?lastUpdatedDate=01012014000000, _kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102, NSLocalizedDescription=The request timed out.}
//-----------------------------------------------------------------------------------
}else{
NSLog(#"%s SESSION ENDED NO ERROR - other delegate methods should also be called so they will reset flags etc", __PRETTY_FUNCTION__);
}
}
There is one method in UIApplicationDelegate,which will let you know about background process.
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
If there are more than one session ,you can identify your session by
if ([identifier isEqualToString:#"com.mydomain.myapp.mySessionIdentifier"])
One more method is used to periodically notify about the progress .Here you can check the state of NSURLSession
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
NSURLSessionTaskStateRunning = 0,
NSURLSessionTaskStateSuspended = 1,
NSURLSessionTaskStateCanceling = 2,
NSURLSessionTaskStateCompleted = 3,
Like you, the app I'm working on always uses a background session. One thing I noticed is that the timeout works properly if it's interrupting a working connection, i.e., the transfer started successfully. However, if I start a download task for a URL that doesn't exist, it wouldn't time out.
Given that you said your backend had been dead for awhile, this sounds a lot like what you were seeing.
It's pretty easy to reproduce. Just set a timeout for like 5 seconds. With a valid URL you'll get some progress updates and then see it timeout. Even with a background session. With an invalid URL it just goes quiet as soon as you call resume.
I have come up to the exact same problem. One solution that i have found is to use two sessions, one for foreground downloads using the default configuration and one for background downloads with background configuration. When changing to the background/foreground generate resume data and pass it from one to the other. But i am wondering if you have found another solution.
I have implemented a NSURLSession that runs in the background (so it can continue the task using the system deamon even when the app is suspended). The issue is that
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
is never called. I need to know the response status so I can handle properly the upload failure. According to another post, an Apple engineer told that this delegate method is not called when the session is backgroundSession to prevent the app from waking. Any suggestion on how to solve this issue? The last URLSession delegate method called in my situation is:
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
The URLSession:task:didCompleteWithError: method of NSURLSessionTaskDelegate should be called when your upload is done. Refer to the task.response object, which should be the NSHTTPURLResponse object.
I'm sure you're doing this, but the standard background upload task components are:
Make a background session:
NSURLSessionConfiguration *backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.domain.app"];
NSURLSession *session = [NSURLSession sessionWithConfiguration:backgroundConfiguration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
Use the NSURLSession method uploadTaskWithRequest:fromFile: method:
NSURLSessionTask *task = [session uploadTaskWithRequest:request fromFile:fileURL];
[task resume];
With a background session, you must:
Use NSURLSessionUploadTask;
Use file-based rendition (you cannot use NSData based version);
Use delegate-based rendition
cannot use data tasks; (b) NSData rendition of the NSURLSessionUploadTask; nor (c) a completion block rendition of the NSURLSessionUploadTask.
With upload tasks, make sure to not call setHTTPBody of a NSMutableRequest. With upload tasks, the body of the request cannot be in the request itself.
Make sure you implement the appropriate NSURLSessionDelegate, NSURLSessionTaskDelegate methods.
Make sure to implement application:handleEventsForBackgroundURLSession: in your app delegate (so you can capture the completionHandler, which you'll call in URLSessionDidFinishEventsForBackgroundURLSession).
I've read through tons of messages saying the same thing all over again : when you use a NSURLConnection, delegate methods are not called. I understand that Apple's doc are incomplete and reference deprecated methods, which is a shame, but I can't seem to find a solution.
Code for the request is there :
// Create request
NSURL *urlObj = [NSURL URLWithString:url];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:urlObj cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30];
[request setValue:#"gzip" forHTTPHeaderField:#"Accept-Encoding"];
if (![NSURLConnection canHandleRequest:request]) {
NSLog(#"Can't handle request...");
return;
}
// Start connection
dispatch_async(dispatch_get_main_queue(), ^{
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]; // Edited
});
...and code for the delegate methods is here :
- (void) connection:(NSURLConnection *)_connection didReceiveResponse:(NSURLResponse *)response {
NSLog(#"Receiving response: %#, status %d", [(NSHTTPURLResponse*)response allHeaderFields], [(NSHTTPURLResponse*) response statusCode]);
self.data = [NSMutableData data];
}
- (void) connection:(NSURLConnection *)_connection didFailWithError:(NSError *)error {
NSLog(#"Connection failed: %#", error);
[self _finish];
}
- (void) connection:(NSURLConnection *)_connection didReceiveData:(NSData *)_data {
[data appendData:_data];
}
- (void)connectionDidFinishDownloading:(NSURLConnection *)_connection destinationURL:(NSURL *) destinationURL {
NSLog(#"Connection done!");
[self _finish];
}
There's not a lot of error checking here, but I've made sure of a few things :
Whatever happens, didReceiveData is never called, so I don't get any data
...but the data is transfered (I checked using tcpdump)
...and the other methods are called successfully.
If I use the NSURLConnectionDownloadDelegate instead of NSURLConnectionDataDelegate, everything works but I can't get a hold on the downloaded file (this is a known bug)
The request is not deallocated before completion by bad memory management
Nothing changes if I use a standard HTML page somewhere on the internet as my URL
The request is kicked off from the main queue
I don't want to use a third-party library, as, ultimately, these requests are to be included in a library of my own, and I'd like to minimize the dependencies. If I have to, I'll use CFNetwork directly, but it will be a huge pain in the you-know-what.
If you have any idea, it would help greatly. Thanks!
I ran into the same problem. Very annoying, but it seems that if you implement this method:
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *)destinationURL
Then connection:didReceiveData: will never be called. You have to use connectionDidFinishLoading: instead... Yes, the docs say it is deprecated, but I think thats only because this method moved from NSURLConnectionDelegate into NSURLConnectionDataDelegate.
I like to use the sendAsynchronousRequest method.. there's less information during the connection, but the code is a lot cleaner.
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
if (data){
//do something with data
}
else if (error)
NSLog(#"%#",error);
}];
From Apple:
By default, a connection is scheduled on the current thread in the
default mode when it is created. If you create a connection with the
initWithRequest:delegate:startImmediately: method and provide NO for
the startImmediately parameter, you can schedule the connection on a
different run loop or mode before starting it with the start method.
You can schedule a connection on multiple run loops and modes, or on
the same run loop in multiple modes.
Unless there is a reason to explicitly run it in [NSRunLoop currentRunLoop],
you can remove these two lines:
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
or change the mode to NSDefaultRunLoopMode
NSURLConnection API says " ..delegate methods are called on the thread that started the asynchronous load operation for the associated NSURLConnection object."
Because dispatch_async will start new thread, and NSURLConnection will not pass to that other threat the call backs, so do not use dispatch_async with NSURLConnection.
You do not have to afraid about frozen user interface, NSURLConnection providing only the controls of asynchronous loads.
If you have more files to download, you can start some of connection in first turn, and later they finished, in the connectionDidFinishLoading: method you can start new connections.
int i=0;
for (RetrieveOneDocument *doc in self.documents) {
if (i<5) {
[[NSURLConnection alloc] initWithRequest:request delegate:self];
i++;
}
}
..
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
ii++;
if(ii == 5) {
[[NSURLConnection alloc] initWithRequest:request delegate:self];
ii=0;
}
}
One possible reason is that the outgoing NSURLRequest has been setup to have a -HTTPMethod of HEAD. Quite hard to do that by accident though!
[edited to provide more information]
(I'm not using AFNetworking for this project. I may do so in future, but wish to resolve this problem/misunderstanding first.)
SERVER SETUP
I cannot provide the real service here, but it is a simple, reliable service that returns XML according to a URL such as:
https://username:password#example.com/webservice
I want to connect to the URL over HTTPS using GET, and determine any authentication failures (http status code 401).
I have confirmed that the web service is available, and that I can successfully (http status code 200) grab XML from the url using a specified username and password. I have done this with a web browser, and with AFNetworking 2.0.3, and by using NSURLConnection.
I have also confirmed that I am using the correct credentials at all stages.
Given the correct credentials and the the following code:
// Note: NO delegate provided here.
self.sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfig
delegate:nil
delegateQueue:nil];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:self.requestURL completionHandler: ...
The above code will work. It will successfully connect to the server, get a http status code of 200, and return the (XML) data.
PROBLEM 1
This simple approach fails in cases where the credentials are invalid. In that case, the completion block is never called, no status code (401) is provided, and eventually, the Task times out.
ATTEMPTED SOLUTION
I assigned a delegate to the NSURLSession, and am handling the following callbacks:
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
if (_sessionFailureCount == 0) {
NSURLCredential *cred = [NSURLCredential credentialWithUser:self.userName password:self.password persistence:NSURLCredentialPersistenceNone];
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
_sessionFailureCount++;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
if (_taskFailureCount == 0) {
NSURLCredential *cred = [NSURLCredential credentialWithUser:self.userName password:self.password persistence:NSURLCredentialPersistenceNone];
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
_taskFailureCount++;
}
PROBLEM 1 WHEN USING ATTEMPTED SOLUTION
Please note the use of ivars _sessionFailureCount and _taskFailureCount. I am using these because the challenge object's #previousFailureCount property is never advanced! It always remains at zero, no matter how many times these callback methods are called.
PROBLEM 2 WHEN USING ATTEMPTED SOLUTION
Despite the use of correct credentials (as proven by their successful use with a nil delegate), authentication is failing.
The following callbacks occur:
URLSession:didReceiveChallenge:completionHandler:
(challenge # previousFailureCount reports as zero)
(_sessionFailureCount reports as zero)
(completion handler is called with correct credentials)
(there is no challenge #error provided)
(there is no challenge #failureResponse provided)
URLSession:didReceiveChallenge:completionHandler:
(challenge # previousFailureCount reports as **zero**!!)
(_sessionFailureCount reports as one)
(completion handler is called with request to cancel challenge)
(there is no challenge #error provided)
(there is no challenge #failureResponse provided)
// Finally, the Data Task's completion handler is then called on us.
(the http status code is reported as zero)
(the NSError is reported as NSURLErrorDomain Code=-999 "cancelled")
(The NSError also provides a NSErrorFailingURLKey, which shows me that the URL and credentials are correct.)
Any suggestions welcome!
You don't need to implement a delegate method for this, simply set the authorization HTTP header on the request, e.g.
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"https://whatever.com"]];
NSString *authStr = #"username:password";
NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding];
NSString *authValue = [NSString stringWithFormat: #"Basic %#",[authData base64EncodedStringWithOptions:0]];
[request setValue:authValue forHTTPHeaderField:#"Authorization"];
//create the task
NSURLSessionDataTask* task = [NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}];
Prompted vs Unprompted HTTP Authentication
It seems to me that all documentation on NSURLSession and HTTP Authentication skips over the fact that the requirement for authentication can be prompted (as is the case when using an .htpassword file) or unprompted (as is the usual case when dealing with a REST service).
For the prompted case, the correct strategy is to implement the delegate method:
URLSession:task:didReceiveChallenge:completionHandler:; for the unprompted case, implementation of the delegate method will only provide you with the opportunity to verify the SSL challenge (e.g. the protection space). Therefore, when dealing with REST, you will likely need to add Authentication headers manually as #malhal pointed out.
Here is a more detailed solution that skips the creation of an NSURLRequest.
//
// REST and unprompted HTTP Basic Authentication
//
// 1 - define credentials as a string with format:
// "username:password"
//
NSString *username = #"USERID";
NSString *password = #"SECRET";
NSString *authString = [NSString stringWithFormat:#"%#:%#",
username,
secret];
// 2 - convert authString to an NSData instance
NSData *authData = [authString dataUsingEncoding:NSUTF8StringEncoding];
// 3 - build the header string with base64 encoded data
NSString *authHeader = [NSString stringWithFormat: #"Basic %#",
[authData base64EncodedStringWithOptions:0]];
// 4 - create an NSURLSessionConfiguration instance
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];
// 5 - add custom headers, including the Authorization header
[sessionConfig setHTTPAdditionalHeaders:#{
#"Accept": #"application/json",
#"Authorization": authHeader
}
];
// 6 - create an NSURLSession instance
NSURLSession *session =
[NSURLSession sessionWithConfiguration:sessionConfig delegate:self
delegateQueue:nil];
// 7 - create an NSURLSessionDataTask instance
NSString *urlString = #"https://API.DOMAIN.COM/v1/locations";
NSURL *url = [NSURL URLWithString:urlString];
NSURLSessionDataTask *task = [session dataTaskWithURL:url
completionHandler:
^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
if (error)
{
// do something with the error
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200)
{
// success: do something with returned data
} else {
// failure: do something else on failure
NSLog(#"httpResponse code: %#", [NSString stringWithFormat:#"%ld", (unsigned long)httpResponse.statusCode]);
NSLog(#"httpResponse head: %#", httpResponse.allHeaderFields);
return;
}
}];
// 8 - resume the task
[task resume];
Hopefully this will help anyone that runs into this poorly documented difference. I finally figured it out using test code, a local proxy ProxyApp and forcibly disabling NSAppTransportSecurity in my project's Info.plist file (necessary for inspecting SSL traffic via a proxy on iOS 9/OSX 10.11).
Short answer: The behavior you describe is consistent with a basic server authentication failure. I know you've reported that you've verified that it's correct, but I suspect some fundamental validation problem on the server (not your iOS code).
Long answer:
If you use NSURLSession without the delegate and include the userid/password in the URL, then completionHandler block of the NSURLSessionDataTask will be called if the userid/password combination is correct. But, if the authentication fails, NSURLSession appears to repeatedly attempt to make the request, using the same authentication credentials every time, and the completionHandler doesn't appear to get called. (I noticed that by watching the connection with Charles Proxy).
This doesn't strike me as very prudent of NSURLSession, but then again the delegate-less rendition can't really do much more than that. When using authentication, using the delegate-based approach seems more robust.
If you use the NSURLSession with the delegate specified (and no completionHandler parameter when you create the data task), you can examine the nature of the error in didReceiveChallenge, namely examine the challenge.error and the challenge.failureResponse objects. You might want to update your question with those results.
As an aside, you appear to be maintaining your own _failureCount counter, but you can probably avail yourself of challenge.previousFailureCount property, instead.
Perhaps you can share some particulars about the nature of the authentication your server is using. I only ask, because when I secure a directory on my web server, it does not call the NSURLSessionDelegate method:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
But rather, it calls the NSURLSessionTaskDelegate method:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
Like I said, the behavior you describe is consist with an authentication failure on the server. Sharing the details about the nature of the authentication setting on your server and the particulars of the NSURLAuthenticationChallenge object might help us diagnose what's going on. You might also want to type the URL with the userid/password in a web browser and that might also confirm whether there is a basic authentication problem.