URLSession background fails with timeout and not retrying after that

I read here that when using `URLSessionConfiguration.background(withIdentifier:)`, it shouldn't timeout and should try again 'till there is an available internet connection or 'till `timeoutIntervalForResource` reached out (which is set to 7 days), but in this case, I'm getting the timeout after less than 15 seconds and after that it doesn't seems to try again, also when there is an internet connection (e.g. turning off Network Link Conditioner).

I haven't set anything to that time so I really don't know why this is happening, all I've done is to set `100% Loss` in Network Link Conditioner to check offline usage (which shouldn’t matter to URLSession since it using background configuration).


In short:
My goal is to start the session whenever there is or there isn't an internet connection and let it fetch as soon as it can, when there is an available internet connection.

What happens right now is that the session is timed out and not performing the session also when there is an internet connection (e.g. turning off Network Link Conditioner).


*It's important to note that when there is an available internet connection while the session is resumed, it does fetching successfully, so the issue is not with the server I'm sending the task to for sure.


Here is my code:

Defining session configuration:

static let urlSessionConfiguration: URLSessionConfiguration = {
    let configuration = URLSessionConfiguration.background(withIdentifier: FilesManager.urlSessionIdentifier)
    configuration.sessionSendsLaunchEvents = false
    configuration.sharedContainerIdentifier = "myApp"
    return configuration
}()

Starting the session:

let url = URL(string: "https://url.com/path")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
urlRequest.httpBody = body

URLSession(configuration: FilesManager.urlSessionConfiguration, delegate: self, delegateQueue: nil).dataTask(with: urlRequest).resume()

Delegate:

extension FilesManager: URLSessionDataDelegate, URLSessionDelegate {
   func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
      print("\(#function) \(error)")
   }
}

Print in console:

urlSession(_:task:didCompleteWithError:) Optional(Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={_kCFStreamErrorCodeKey=-2104, _NSURLErrorFailingURLSessionTaskErrorKey=BackgroundDataTask .<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "BackgroundDataTask .<1>",
    "LocalDataTask .<1>"
), NSLocalizedDescription=The request timed out., _kCFStreamErrorDomainKey=4, NSErrorFailingURLStringKey=https://url.com/path, NSErrorFailingURLKey=https://url.com/path})



If you know a better way to achieve my goal or you’re think there is something wrong in this code, please, let me know.


Thanks,

Ido.

Answered by DTS Engineer in 706050022

Do you know of any sample code, which shows a good way of handling this?

That depends on the lifecycle of your app. If you’re using a traditional app delegate, adding code to application(_:willFinishLaunchingWithOptions:) is a good option.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

You're testing with a POST request and

URLSession
won’t retry such requests because they are not idempotent. See Section 4.2.2 of RFC 7231 for more background to this..

Share and Enjoy

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

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

Oh, so do you have another way to achieve this goal? (I have to use POST 'cause I'm using `multipart/form-data`)

How big is your HTTP body you’re posting?

Share and Enjoy

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

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

It really depends on the resolution of the image, it can be between 0.1Mb to 1MB (maybe a bit more).


Anyway, I got some suggestion that based on what you've said (that only GET requests are retrying to be sent 'till success), and tried to send some `ping` with GET (tried also with HEAD) request so only when `urlSession(_:task:didCompleteWithError:)` will beign called, I'll send the POST request (and know that there is an internet connection), but from some reason I'm getting timeout also for the GET request (that uses the same background configuration).


Please tell me, am I doing something wrong?

This is my code:


In class:

static var requests = [URLRequest]()
static let urlSessionConfiguration: URLSessionConfiguration = {  
    let configuration = URLSessionConfiguration.background(withIdentifier: FilesManager.urlSessionIdentifier)  
    configuration.sessionSendsLaunchEvents = false  
    configuration.sharedContainerIdentifier = "myApp"  
    return configuration  
}() 

In upload func:

let url = URL(string: "https://url.com/path")! 
var urlRequest = URLRequest(url: url) 
urlRequest.httpMethod = "POST" 
urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 
urlRequest.httpBody = body 

FilesManager.requests.append(urlRequest)
URLSession(configuration: FilesManager.urlSessionConfiguration, delegate: self, delegateQueue: nil).dataTask(with: URL(string: "https://url.com/ping.txt")!).resume()

Delegate:

extension FilesManager: URLSessionDataDelegate, URLSessionDelegate { 
   func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 
      print("\(#funcion) \(error), \(task.originalRequest?.httpMethod)")
      guard FilesManager.requests.count > 0 else { return }
      let urlRequest = FilesManager.requests.removeFirst()
      FilesManager.urlSession.dataTask(with: urlRequest).resume()
   } 
}


Console:

urlSession(_:task:didCompleteWithError:) Optional(Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={_kCFStreamErrorCodeKey=-2104, _NSURLErrorFailingURLSessionTaskErrorKey=BackgroundDataTask .<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "BackgroundDataTask .<1>",
    "LocalDataTask .<1>"
), NSLocalizedDescription=The request timed out., _kCFStreamErrorDomainKey=4, NSErrorFailingURLStringKey=https://url.com/ping.txt, NSErrorFailingURLKey=https://url.com/ping.txt}), Optional("GET")
urlSession(_:task:didCompleteWithError:) Optional(Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={_kCFStreamErrorCodeKey=-2104, _NSURLErrorFailingURLSessionTaskErrorKey=BackgroundDataTask .<2>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "BackgroundDataTask .<2>",
    "LocalDataTask .<2>"
), NSLocalizedDescription=The request timed out., _kCFStreamErrorDomainKey=4, NSErrorFailingURLStringKey=https://url.com/path, NSErrorFailingURLKey=https://url.com/path}), Optional("POST")

After some searching I found that background uploads works only when uploading from file, so I saved the `URLRequest.httpBody` as a file and uploaded it:


static func urlSessionConfiguration(for id: String) -> URLSessionConfiguration {
    let configuration = URLSessionConfiguration.background(withIdentifier: FilesManager.urlSessionIdentifierPrefix + id)
    configuration.sharedContainerIdentifier = FilesManager.urlSessionIdentifierPrefix
    return configuration
}

let session = URLSession(configuration: FilesManager.urlSessionConfiguration(for: fileName), delegate: self, delegateQueue: nil)
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let filePath = "\(documentsPath)/\(fileName)"
DispatchQueue.main.async {
    if let data = urlRequest.httpBody, (data as NSData).write(toFile: filePath, atomically: true) {
       urlRequest.httpBody = nil // Use only file's content
       let fileUrl = NSURL(fileURLWithPath: filePath) as URL
       session.uploadTask(with: urlRequest, fromFile: fileUrl).resume()
    } else {
       session.uploadTask(withStreamedRequest: urlRequest).resume()
    }
}


Now I'm facing an issue that if the user is manually quit the app (from the app switcher), the background uploads are cancled (that I've found in some article: "if you manually force quit the app then iOS will take this as definite confirmation that you are finished with the app and so cancel any scheduled background-transfers.").

Well, as one of many users how quiting apps like this when we were done with (or at least we're think so, 'cause we're not aware to background tasks), I can tell that it is a really big issue.

The article I've mentioned does suggest a way to "overcome" this issue, but it only for testing, I can't really detect when the app has been quit from the app switcher (as much as I know).


So my question is:

Can I prevent from background tasks to be cancelled when the app is quit from the app switcher (or at least to detect that from `AppDelegate` and run `exit()` from code to overcome this)?

Can I prevent from background tasks to be cancelled when the app is quit from the app switcher

No.

Assuming you’re on a modern systems (iOS 8 and later), the next time your app is launched the task will complete with a

NSURLErrorCancelled
error, and that error will most likely contain a
NSURLErrorBackgroundTaskCancelledReasonKey
entry in its user info with
NSURLErrorCancelledReasonUserForceQuitApplication
. You could use that an as opportunity to educate your users as to why force terminating an app is a bad idea.

Share and Enjoy

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

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

OK, thats a good direction.

Can I retry to execute the `uploadTask` from the delegate?

Will calling `resume()` on `task: URLSessionTask` work or after the task has been canceled it useless?


Edit:

It doesn't seems to fire `urlSession(session:task:didCompleteWithError:)` after opening the app after force quitting it (I'm able to see `NSLog` output from other methods, but not from the delegate),

I can't even log the error to see if it contains the keys you've mentioned.


You may see my logs:


16:47:19.249330 +0300
My app
Debug: viewDidLoad()

16:51:57.128801 +0300
My app
Debug: uploadImage(_:to:completionBlock:), Upload task resumed

16:52:07.594877 +0300
My app
Debug: urlSession(_:task:didCompleteWithError:)

16:52:40.115190 +0300
My app
Debug: uploadImage(_:to:completionBlock:), Upload task resumed


### Force Quit here, not getting to `urlSession(_:task:didCompleteWithError:)`
### Open the app again:


16:52:49.592161 +0300
My app
Debug: viewDidLoad()

Can I retry to execute the

uploadTask
from the delegate?

Yes.

Will calling

resume()
on
task: URLSessionTask
work … ?

No. Once a task has run to completion it can’t be resumed.

With regards getting the error:

  • Make sure you run the app from outside of Xcode. See my Testing Background Session Code pinned post for more info about this.

  • Are you user you re-created your background session? The most common cause of problems like this is folks lazily creating their background session, which causes problems when their app is relaunched after being terminated in the background (even in the non-force-quit case) because they never actually reconnect to the background session.

Oh, and switch to using

os_log
.
NSLog
is fine for a quick hack, but debugging background sessions is a tricky business and you want to use a logging system that you can leave enabled in your production build (so that you can investigate bug reports that come in from the field).

Share and Enjoy

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

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

I saw your post and also tried to use `os_log` after one of your comments, but it didn't worked for me from some reason so I switched to `NSLog`.


What do you mean by "Are you user you re-created your background session?" where should I recreate it and how? as much as I understood it should call the delegate automatically, 'cause it is a background process that can't "die" (even after force quit).

I saw your post and also tried to use

os_log
after one of your comments, but it didn't worked for me from some reason so I switched to
NSLog
.

I recommend that you take the time to get

os_log
working; it’s pretty foundational IMO.

What do you mean by "Are you user you re-created your background session?"

Sorry, that was a typo. I meant to say Are you sure you re-created your background session?

as much as I understood it should call the delegate automatically, 'cause it is a background process that can't "die"

I think you’ve misunderstood the

URLSession
background session lifecycle. Here’s a quick summary:
  1. You start a request.

  2. That request is passed to the

    URLSession
    background session daemon (currently called
    nsurlsessiond
    ).
  3. That daemon starts working on the request on your behalf.

  4. The user moves your app to the background.

  5. It gets suspended.

  6. At this point one of two things can happen. Let’s start with the simple case, namely, that your request finishes while your app is suspended.

  7. The system resumes your app in the background.

  8. The system starts calling session delegate methods, most notably

    urlSession(_:task:didCompleteWithError:)
    .

However, step 6 suggests another possibility:

  1. Steps 1 through 5, as above.

  2. The system gets short on memory and terminates your app (this is not a force quit, it’s the standard remove-a-suspended-app-from-memory termination).

  3. Your request finishes.

  4. The system relaunches your app in the background.

  5. During the launch process, your app re-creates its

    URLSession
    object.
  6. The system starts calling session delegate methods, most notably

    urlSession(_:task:didCompleteWithError:)
    .

It’s common for folks to make the mistake of creating their

URLSession
lazily, either deliberately (as an optimisation) or accidentally (because they’ve tied the session to a view controller, which is a really bad idea). If so, they’re never re-create the session in the relaunch case, and thus they encounter weird problems.

The force quit case is similar, except that:

  • In step 3, your request doesn’t finish, it gets cancelled by the system.

  • In step 4, the system doesn’t relaunch your app in the background, but rather you have to wait for the user to manually relaunch it.

Share and Enjoy

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

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

Got it, but I'm missing the answer for "Where should I recreate it and how?"

Also, I'm not sure that I understood this:

It’s common for folks to make the mistake of creating their URLSession lazily, either deliberately (as an optimisation) or accidentally (because they’ve tied the session to a view controller, which is a really bad idea).

What do you mean by "creating URLSession lazily"?

How can I create it in another way? I know that I need to create it only after some action the user performed, so it will always be "lazily" (or I didn't got it correctly).

I'm creating it and attaching the delegate to a class that I've created (class FilesManager: NSObject, URLSessionDataDelegate, URLSessionDelegate{}).


If not using `self` for the delegate, what else can I use?

How can I pass delegate as some "`static class`" so it will always be avaliable to fire delegate's methods?


Sorry for asking so much, but I couldn't found answers for this thing anywhere and the docs are missing most of the things you've mentioned since your first comment.

What do you mean by "creating URLSession lazily"?

A common pattern might be something like this:

// DO NOT DO THIS!

class Main: NSObject, URLSessionDelegate {

    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(withIdentifier: …)
        return URLSession(configuration: config, delegate: self, delegateQueue: .main)
    }()

    func downloadTask(for request: URLRequest) -> URLSessionDownloadTask {
        return self.session.downloadTask(with: request)
    }
}

// DO NOT DO THIS!

This works fine for standard sessions but it’s broken for background sessions because, when your app is terminated, you don’t re-create the session on relaunch. However, a lot of the time folks don’t notice this because they don’t actually test the relaunch case.

Another common mistake is to associate the background session with a view controller. If your app is relaunched in the background it’s not uncommon for that view controller to not be instantiated, and thus the session never gets re-created.

The approach I recommend is to have an object (a model-level controller object) that is responsible for the session, and explicitly, and unconditionally, instantiate that during your app’s launch sequence [1].

Share and Enjoy

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

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

[1] There are places where this gets tricky:

  • Sessions shared with another process — See Networking in a Short-Lived Extension for more on that.

  • Dynamically allocated background session identifiers — In this case you need to keep your own records of the the identifiers in play so that you can reconnect to each session on app launch. IMO it’s much better to stick to static identifiers.

Sorry to dig up this old thread here, but I just stumbled across the question of how best to recreate the session when the app is launched, as this is only mentioned in the documentation as an aside.

"Funny" enough, the sample code to create the session in the documentation is the following:

private lazy var urlSession: URLSession = {
    let config = URLSessionConfiguration.background(withIdentifier: "MySession")
    config.isDiscretionary = true
    config.sessionSendsLaunchEvents = true
    return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()

Do you know of any sample code, which shows a good way of handling this?

Accepted Answer

Do you know of any sample code, which shows a good way of handling this?

That depends on the lifecycle of your app. If you’re using a traditional app delegate, adding code to application(_:willFinishLaunchingWithOptions:) is a good option.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I was sure that I've posted here the solution, but better later than never...

Since it's a pretty long code, I've made a gist for it (hopefully it doesn't missing any custom helper): https://gist.github.com/Idomo/ece6683348372b54e470fd630b1fb76e

Beside that, to recreate the saved unfinished sessions, so it'll fire urlSession(_:task:didCompleteWithError:) and re-upload, you should call this function in application(_:, didFinishLaunchingWithOptions:):

func recreateFilesUploadTasks() {
    if let filesUploadTasks = UserDefaults.standard.stringArray(forKey: UserDefaultsKeys.filesUploadTasks) {
        let filesManager = FilesManager()
        for sessionId in filesUploadTasks {
            filesManager.recreateSession(id: sessionId)
        }
    }
}
URLSession background fails with timeout and not retrying after that
 
 
Q