NSURLSessionTask never calls back after timeout when using background configuration
Asked Answered
T

5

20

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,

Tedmann answered 25/4, 2014 at 9:8 Comment(8)
Did you ever find a solution to this problem?Eldenelder
Yes and no. No: I reported the bug to Apple who was kind enough to answer that my bug report wasn't complete enough. I was too lazy to provide a test that times out. Pretty hard to do.Tedmann
Yes: My problem was when using the background sessions where GUI would be waiting for a callback. In that case that's annoying if it does not call back because you can't update your UI. So what I did was NOT use background sessions when dealing with network calls initiated from the UI and needing feedback.Tedmann
Long story short: I use background sessions only to perform sync in background, and the good old AFNetworking when dealing with network calls that have GUI feedback. Do I answer your question?Tedmann
Does feel very much like an Apple bug. I find that I do not get a call back if I providing url to a non responsive server. For now I have just added my own timer set to timeout + 1 and handle it myself, but I it would be nice for a more elegant fix. Thanks for your reply good to no i'm not going crazy.Eldenelder
If you come up with an URL that is easy to get timeout from, don't hesitate to bundle a simple example and send it via the bug tracker, otherwise they will probably never fix it.Tedmann
I have filed a bug as it was very easy to reproduce using httpbin.org/delay/10 and setting request timeoutInterval to 5. Request with NSURLConnection times out as expected after 5 seconds but NSURLSession completes the request taking 10 seconds.Eldenelder
Yes that's a nice one! The one I wanted to reproduce was that if your web page NEVER times out, NSURLSession will never timeout either. But I guess it can be deduced from your test :)Tedmann
H
21

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.

Huberman answered 12/2, 2016 at 8:2 Comment(5)
That's very precise and interesting. Is that some kind of insider's information? ;) (Not asking only because I'm curious but because I would like to validate the answer)Tedmann
I got this answer from one of the members of Apple Staff at the developer forum. Also, I have verified this by implementing.Huberman
That's great, thank you @utsav-dusad . Welcome on Stackoverflow :) It's always great to mention where your information comes from. In that case we can only regret that the BackgroundSession documentation is not complete as it seems to me that this information belongs there.Tedmann
My experiments confirm that setting timeoutIntervalForResource does its job. didCompleteWithError is correctly called after the timeout. @utsav-dusad, you rock! if it is still not called for someone, make sure you are aware about Xcode bug discussed here: #39496273Weak
It would be good if you add your source for your answer that you copied from: forums.developer.apple.com/thread/22690Czechoslovakia
D
4

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__);
    }
}
Daily answered 18/1, 2016 at 16:4 Comment(4)
Ok, so this is the timeout between two chunks of data, after the connection has been established. What about another sort of timeout, which is more frequent: When the requested URL does not even respond? In this case, the timeout is the time you wait for a (header) response from the HTTP server before you give up. In many languages/libraries these timeouts are treated differently.Tedmann
I must admit that I haven't tried for a while so Apple may very well have fixed that by now.Tedmann
Here is a link to a Java-related question discussing different types of timeouts: #18185399Tedmann
Two im handling at the moment were: -1001(The request timed out). -1005 "The network connection was lost.[i think triggered by tester leaving range of wifi from my mac]Daily
E
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,              
Elsieelsinore answered 25/4, 2014 at 11:15 Comment(4)
Thank you, but same answer than user2260054. I do implement this callback [and it is not called], that is not the problem. The callback you point out is used only when your application is going to start in background to tell you to create the session with the provided identifier. And to call the provided callback when you're done handling the session's tasks. The problem I point out happens both when the app is in back and foreground, when using the so called "background" NSURLSession. The callback you point out is never called when your app is in foreground.Tedmann
In NSURLSession delegate method,you can check the state of NSURLSessionElsieelsinore
Yes. But the whole thing is to try and download from a server that does not respond (timeout). Thus didWriteData should not be called as the server will never return any data. I'm wondering why the request never calls back when running on a NSURLSession with background configuration, while it will when running on the default configuration. All requests should call back somehow someday.Tedmann
yes.It will not be called because of time out.SO the progress will also be stopped and -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error method will be called .From here u can know the reason of error.whether it is a time out issue or any network issue.Elsieelsinore
D
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.

Diplopod answered 2/3, 2015 at 4:45 Comment(1)
Thanks for your input. That's why on most frameworks you often have two or even three separate timeout types. At least the connection establishment timeout (the one that does not work) and the data transfer timeout (the one you're talking about that's working). Sometimes you even have a third timeout for the delay between "the request was sent" and "the first byte was received".Tedmann
O
0

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.

Ornithopod answered 27/8, 2015 at 9:7 Comment(3)
We were using the good old way with HttpClient before so we kept using it in foreground, and used background sessions in background. Takes creates some very messy code and double testing. Your solution seems much more attractive to me.Tedmann
Keep in mind though that their may still exist requests that are initiated when the app is active but that you would want to do in background anyway (because they won't stop executing if the user sends the app in background). That would be the case for all requests that the UI doesn't depend on.Tedmann
I haven't tested this for a while, it's a pity to hear that the problem still hasn't been addressed.Tedmann

© 2022 - 2024 — McMap. All rights reserved.