NSURLSessionTask never calls back after timeout wh

2020-05-19 05:45发布

问题:

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,

回答1:

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.



回答2:

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__);
    }
}


回答3:

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,              


回答4:

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.



回答5:

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.