Careful with NSURLSession
After writing a nice downloader for iOS using the brand new Apple NSURLSession set of classes I learned quite a lot of things, and I tried to apply similar concepts in an uploader class for OS X 10.9, here's what I found.
Reconnect your URL Session ASAP
For background downloads on iOS there are special delegate callbacks to implement to know when a job does finish.
For uploads on OS X apparently you just create singleton of your main class and your session configuration and provide to instantiate it at the start of the app with the same identifier.
Even the backgroundSession should be a singleton.
- (NSURLSession *)backgroundSession {
/**
* Using dispatch_once here ensures that multiple background sessions with the same identifier are not
* created in this instance of the application. If you want to support multiple background sessions
* within a single process, you should create each session with its own identifier.
*/
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"ch.icoretech.BackgroundTransferSession"];
[configuration set...];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session;
}
If an upload transfer fails for specific reasons, the file you are trying to upload will be DELETED
This caught me by surprise.
I was in the middle of building the uploader so my session configuration (and related custom requests), wasn't exactly great and would fail for sure (wrong remote URLs, etc.).
Problem is that after some quick CMD+R I noticed in the console that some files were failing syncing, although I was pretty sure they were there.
Perhaps it was a sandboxing problem, so I rerun the stuff until I figured it out: the files were really missing from the filesystem. ARGH!
It was my iTunes library I was trying to upload, half of it it's gone. Luckily I'm building this stuff for AudioBox, and my library has been backed up so I was able to recover in case you're wondering.
I haven't found in the documentation for this behavior, all I know is that:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
gets called when a task finishes, and it doesn't matter if the status code is in the bad range, error will be nil. But if error
is present it means a "client error" occurred, and the file will be gone from filesystem when the operation is over.
Without writing out a bad request and observe this with my eyes first, there was a high risk to put someone's file at danger should the request fail for some reason. I'm glad I caught this before publishing the app.
Only solution I have found it to copy the file in a safe temporary zone within the sandboxed app and play safely on that. I cleanup this copy once I know that the request succeeded.
For big libraries this might be a problem and probably I'll just add items to the queue in a throttled manner.
For downloads you provide delegate callbacks where to effectively copy the downloaded file to a safe location. Perhaps this ephemeral concept applies also to uploads.
Apple rewrites your request body
This is another interesting one, while you can pass a custom NSURLRequest to the background jobs, the body, when sending out a file with:
uploadTaskWithRequest:fromFile
will be erased and replaced with the actual file binary data, as per documentation.
This means that even if you prepare a multipart request, this won't work, because only the multipart headers will be found in the outgoing http/https call, and servers will return an error since the body does not conform to multipart protocol.
Conclusions
There's a lot more to say about what I discovered, even at lower levels, how those classes works on OS X, and few other bugs, but I guess I'll leave them for another protip.
Written by Claudio Poli
Related protips
1 Response
Hi I have a similar problem while uploading video files in the background. I have multiple files to be uploaded in the background. Lets assume video1 is being uploaded. Now if a user decided to pause the upload for video1 I make a call to uploadTask suspend and then create a new task in the same background session using
self.uploadTask = [self.session uploadTaskWithRequest:afRequest fromFile:[NSURL fileURLWithPath:dataPath]];
[self.uploadTask resume];
The second file starts uploading but somehow does not work in the background. Please help me on this