NSURLSessionDownloadDelegate methods not called after app resume

I'm seeing some strange behavior with a background NSURLSession. My app starts a number of download tasks, and everything initially works as expected. There are repeated calls to URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: and URLSession:downloadTask:didFinishDownloadingToURL. But after suspending the app by pressing the home button and then resuming it, the delegate methods stop getting called regularly. This happens roughly 50% of the time I suspend and resume the app.


If I leave the app active, the downloads do eventually complete. The app delegate's handleEventsForBackgroundURLSession method is called, didFinishDownloadingToURL is called a bunch of times, and then finally the URLSessionDelegate receives the URLSessionDidFinishEventsForBackgroundURLSession call. This is the behavior that I'd expect to see if the app were not active.


While the downloads do eventually complete, there's no way to report progress to the user when the delegate methods aren't called. Has anyone else encountered a problem similar to this?


—Chris

Replies

I’ve seen folks have this problem before because they don’t re-create their NSURLSession background session on app launch. So this sequence:

  1. Move to background

  2. Suspend

  3. Resume

  4. Move to foreground

works, because the app never gets terminated, but this sequence:

  1. Move to background

  2. Suspend

  3. OS terminates app

  4. Relaunch

fails because the app doesn’t re-create the session. Then, when

-application:handleEventsForBackgroundURLSession:completionHandler:
gets call the app re-creates its background session and starts getting events again.

So, my recommendation is that you make sure your app re-creates its NSURLSession background session on launch.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

It's actually the first sequence that's *not* working.


The session doesn't need to be recreated because the app isn't being terminated. About half the time the app is resumed, the delegate methods are called as expected. A couple suspend/resume cycles generally unsticks the downloader, but that's not ideal for our users.


—Chris

It's actually the first sequence that's not working.

I’ve not seen that behaviour before. I suspect that there’s something specific going on with your app; if this were affecting a lot of apps out there, I’d have seen reports from others.

My best guess is that your app is doing something odd that’s causing the background session state to get out of whack. Perhaps:

  • Accidentally dropped the completion handler passed to

    -application:handleEventsForBackgroundURLSession:completionHandler:
  • Getting a thread stuck in an NSURLSession delegate callback

  • Or something similar

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you for the suggestions. I don't think I'm doing either of those things, but I'll continue to look.


The completion hander does not seem to be the problem: after the call to application:handlerEventsForBackgroundURLSession:completionHandler is received, all the didFinishDownloadingToURL calls arrive at once and the app successfully copies the files into place. The problem is that no delegate methods are called before this—it all gets queued into a single burst after all file downloads are complete.


—Chris

Excuse me.Do you solve this problem?

I'm also interested in whether or not you were able to solve this, and if so how? I'm experiencing the same behavior where after re-opening my application (and it is still in memory and running the downloads in the background) I stop receiving the delegate callbacks and then all of the didFinishDownloading etc. come through when all completes. My big issue is this prevents my didWriteData from being called and I am unable to report progress back to the user after they've re-opened the application.


Thank you in advance!

@eskimo - You’re reply to @claurel was the following:


My best guess is that your app is doing something odd that’s causing the background session state to get out of whack. Perhaps:

• Accidentally dropped the completion handler passed to -application:handleEventsForBackgroundURLSession:completionHandler:

• Getting a thread stuck in an NSURLSession delegate callback

• Or something similar


Could you explain what you mean by “accidentally dropped the completion handler in point # 1 above? I believe I’m handling the handleEventsForBackgroundURLSession properly but want to make sure.


Also what would cause point # 2 of yours above to occur?


Thanks!

I have decided to start my own thread on this issue where I can provide more details on what I am personally experiencing with different devces on different versions of iOS: https://forums.developer.apple.com/thread/77666

Could you explain what you mean by “accidentally dropped the completion handler in point # 1 above?

The standard sequence for being resumed in the background for NSURLSession background session events is:

  1. The system resumes (or relaunches) your app

  2. If it’s a relaunch, you re-create your background session

  3. The system calls

    -application:handleEventsForBackgroundURLSession:completionHandler:
    to tell you what session to service
  4. You save away the completion handler

  5. The session starts sending you delegate callbacks

  6. When it’s done it sends

    -URLSessionDidFinishEventsForBackgroundURLSession:
  7. You call the completion handler you saved in step 4

The gotcha I was specifically referring to relates to steps 4 and 7. If you fail to save the completion handler (step 4), or fail to call it (step 7), or do something else wrong (like accidentally clear it, or overwrite it), weird things are going to happen.

Another gotcha relates to the thread on which you call the completion handler. It must be called from the main thread. If you have your NSURLSession delegate set up to run on a secondary thread, you can’t call the completion handler directly from

-URLSessionDidFinishEventsForBackgroundURLSession:
, but instead must bounce to the main thread to do that job.

Also what would cause point # 2 of yours above to occur?

Imagine a scenario like this:

  1. You start a download

  2. The download completes and your

    -URLSession:downloadTask:didFinishDownloadingToURL:
    method is called
  3. That has to commit the download to your database

  4. There’s some sort of other problem that causes that commit to deadlock

At this point you’re permanently blocked within the delegate callback, which is going to prevent NSURLSession sending your any other delegate callbacks.

A similar but harder-to-debug problem relates to delegate callbacks with completion routines. Consider this:

  1. You start a download

  2. The download encounters an authentication challenge

  3. The system calls your authentication challenge delegate callback (

    -URLSession:didReceiveChallenge:completionHandler:
    or
    -URLSession:task:didReceiveChallenge:completionHandler:
    )
  4. That callback fails to call the completion handler

Now this request is just stalled; it won’t even time out because all the timeout timers are suspended while waiting for the authentication challenge to be resolved.

I’ll respond to your big picture issue on your new thread.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I have the same issue on iOS 12.0. Is there any solution in sight???

I'm finding this problem to be 100% reproducable when running on an iPhone 5s with iOS 12. Has anyone found a resolution?

I have get the same problem on iOS 12, Has anyone konw about what is going on? There is my simple code project ( https://github.com/hwzss/BackgroundSessionProblemExample )

As hacky as this is, I found that briefly suspending and resuming the download task gets progress events firing again. I do this in response to UIApplicationDidBecomeActiveNotification. I'm not sure if all the thread dispatching is necessary but it gets the job done.


    [self.sharedBackgroundSession getAllTasksWithCompletionHandler:^(NSArray<__kindof NSURLSessionTask *> * _Nonnull tasks) {
       
        for (NSURLSessionTask *sessionTask in tasks)
        {
            if (sessionTask.state == NSURLSessionTaskStateRunning)
            {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [sessionTask suspend];
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        [sessionTask resume];
                    });
                });
            }
        }

    }];

Tested on physical devices running iOS 12.4.6 and 13.4.