Background Upload With Stream Request Using NSUrlSession in iOS8 - ios

Previously in iOS7 when we try to upload with stream request in background we get following exception
Terminating app due to uncaught exception 'NSGenericException', reason: 'Upload tasks in background sessions must be from a file'
But in iOS8 there is no exception when we try to upload with stream in background.
Now my question is
1) Is backgourd upload with uploadTaskWithStreamedRequest: allowed in iOS8?
2) In iOS8 i am using background NSURLConfiguration with uploadTaskWithStreamedRequest. I am using -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *))completionHandler to provide stream to NSUrlSession. When app is in foreground it is working fine and uploading my file to the server. But as soon as the app igoes in background the stream ends and NSURLSession completes with following error
Error Domain=NSURLErrorDomain Code=-997 "Lost connection to background transfer service"
I think when app goes in background my stream ends. Now my question is that in which runloop should I should I schedule my Stream or let me know if there is any mistake in my understanding.
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *))completionHandler
{
// Open producer/consumer streams. We open the producerStream straight
// away. We leave the consumerStream alone; NSURLConnection will deal
// with it.
NSLog(#"%#", [NSThread currentThread]);
NSInputStream *consStream;
NSOutputStream *prodStream;
[NSStream createBoundInputStream:&consStream outputStream:&prodStream bufferSize:SFAMaxBufferLength];
assert(consStream != nil);
assert(prodStream != nil);
self.consumerStream = consStream;
self.producerStream = prodStream;
self.producerStream.delegate = self;
[self.producerStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.producerStream open];
// Set up our state to send the body prefix first.
self.buffer = [self.bodyPrefixData bytes];
self.bufferLimit = [self.bodyPrefixData length];
completionHandler(self.consumerStream);
}

You can't upload streamed tasks using Background Configuration. I successfully upload data only in two cases:
Download task with data stored in request body.
Upload task from file. In that case you will not receive response body.

You can upload a multipart file in background - just that this is not straight forward. Refer: AFNetworking error in uploadTaskWithStreamedRequest

Related

iOS-WatchKit File Transfers Work Unreliably

I've built an app for iOS 9 and WatchOS 2. The iOS app will periodically transfer image files from the iPhone to the Watch. Sometimes, these are pushed from the app, sometimes the Watch requests (pulls) them. If pulled, I make the requests asynchronous, and use the exact same iOS code to transfer images in both cases.
About half the time (maybe 2/3), the file transfer works. The other times, it appears that nothing happens. This is the same whether I'm pushing or pulling images.
On the iOS side, I use code similar to this (session activated already):
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
if (session.reachable) {
NSData *imgData = UIImagePNGRepresentation(img);
NSURL *tempFile = [[session watchDirectoryURL] URLByAppendingPathComponent: #"camera.png"];
BOOL success = [imgData writeToFile: [tempFile path] atomically: NO];
if (success) {
NSLog(#"transferFile:metadata:");
[session transferFile: tempFile metadata: nil];
} else {
NSLog(#"will not call transferFile:metadata:");
}
} else {
NSLog(#"Camera watch client not reachable.");
}
}
On the watch extension side, I have a singleton that activates the watch session and receives the file:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file {
// pass the data file to the data listener (if any)
[self.dataListener session: session didReceiveFile: file];
}
My "data listener" converts the file to a UIImage and displays it on the UI thread. However, that's probably irrelevant, as the unsuccessful operations never get that far.
During unsuccessful transfers, session:didReceiveFile: is never called. If I inspect the iOS app's log, however, I see these messages only during the operations that fail:
Dec 26 15:10:47 hostname companionappd[74893]: (Note ) WatchKit:
application (com.mycompany.MyApp.watchkitapp), install status: 2,
message: application install success
Dec 26 15:10:47 hostname
companionappd[74893]: (Note ) WatchKit: Purging
com.mycompany.MyApp.watchkitapp from installation queue, 0 apps
remaining
What is happening here? It looks like the app is trying to reinstall the Watch app (?). When this is happening, I do not see the watch app crash/close and restart. It simply does nothing. No file received.
On the iOS side, I scale down the image to about 136x170 px, so the PNG files shouldn't be too big.
Any ideas what's going wrong?
Update:
I have posted a complete, minimal project that demonstrates the problem on Github here
I am now under the impression that this is a bug in the simulators. It seems to work more reliably on the Apple Watch hardware. Not sure if it's 100% reliable, though.
Apple bug report filed (#24023088). Will update status if there is any, and leave unsolved for any potential answers that may provide workarounds.
For me, not a single transfer was working anymore. Polling transfer.progress showed isTransferring == true, but I never got beyond 0 completed units.
I ended up:
Deleting apps on watch and iPhone
Rebooting both
Reinstalling
And it works.
This is how I managed to transfer files from phone to watch:
In order for this to work, the file must be locate in appGroupFolder, and "App Groups" must be enabled from Capabilities tab, for phone and watch.
In order to get appGroup folder use following line of code:
NSURL * myFileLocationFolder = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier: #"myGroupID"]; //something like group.bundle.projName
Once you got that use this to send message and handle response from watch:
[session sendMessage:#{#"file":myFileURL.absoluteString} replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
//got reply
} errorHandler:^(NSError * _Nonnull error) {
//got Error
}];
Even though WCSession *session = [WCSession defaultSession]; I have noticed that sometimes session is deallocated, so you might consider using [WCSession defaultSession]; instead.
To catch this on the phone use:
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler{
//message[#"file"] - addres to my file
//do stuff with it here
replyHandler(#{#"myResponse":#"responseData"}); //this call triggers replyHandler block on the watch
}
Now a if you didn't forget to implement WCSessionDelegate and use
if ([WCSession isSupported]) {
_session = [WCSession defaultSession];
_session.delegate = self;
[_session activateSession];
}
//here session is #property (strong, nonatomic) WCSession * session;
It all should work.
Made a broader answer, hopefully will reach out to more people.

NSURLSession download task failing after it already succeeded

I am experiencing a strange issue from time to with my NSURLSessionDownloadTasks (using a background download configuration).
It is always related to errors with NSURLDomain code NSURLErrorBackgroundSessionWasDisconnected (-997). The error itself usually seems to be coming from the app generically crashing or being forcefully closed during development from Xcode.
The relevant code boils down to
- (void)download:(NSURLRequest *)request
{
NSURLSessionDownloadTask *downloadTask = [self.urlSession downloadTaskWithRequest:request];
[downloadTask resume];
}
- (void) URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)tempLocation
{
NSLog(#"task %# didFinishDownloadingToURL with status code %#", downloadTask, #([downloadTask.response statusCode]));
}
- (void) URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
NSLog(#"task %# didCompleteWithError %#", task, error);
}
The problem in these cases is, that the same NSURLSessionDownloadTask gets two (instead of only one expected) call in -[NSURLSessionTaskDelegate URLSession:task:didCompleteWithError:]. Once without an error (which aligns to a call with a 200 OK response code to -[NSURLSessionDownloadDelegate URLSession:downloadTask:didFinishDownloadingToURL:]) and then another time, usually a couple seconds later with an error (-997). Both times it's exactly the same memory address for the task being passed into these delegate methods.
Has anybody else experienced something similar? I am not expecting that second callback after I was already told that my task succeeded. Is there any obvious reason why the NSURLSession may still hold on to a task it reportedly finished already but then all of a sudden thinks it should inform me that it's lost connectivity to it background transfer service?
FWIW, I had a similar issue because I was updating the UI from the NSURLSession delegate methods, which run in background thread.
So, the solution for me was to move the UI-related code to run on the main thread as below:
dispatch_async(dispatch_get_main_queue(), ^{
//code updating the UI.
});

NSURLDomainErrorDomain error -999 when app terminate with NSURLSession

I have big trouble with NSURLSession when i'll terminate the App.
I have downloaded the apple sample:
https://developer.apple.com/library/ios/samplecode/SimpleBackgroundTransfer/Introduction/Intro.html
on Apple reference.
When i start download the file download correctly.
When i enter in background the download continues to.
When i terminate the application and i restart the app the application enter in:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
And i catch this error:
The operation couldn't be completed. (NSURLErrorDomain error -999.)
It seems that i cannot restore download when app has been terminated. It's correct?For proceed with download i must leave application active in background?
Thank you
Andrea
A couple of observations:
Error -999 is kCFURLErrorCancelled.
If you are using NSURLSessionDownloadTask, you can download those in the background using background session configuration, e.g.
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundIdentifier];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
If not using background session (e.g. you have to use data task, for example), you can use beginBackgroundTaskWithExpirationHandler to request a little time for the app the finish requests in the background before the app terminates.
Note, when using background sessions, your app delegate must respond to handleEventsForBackgroundURLSession, capturing the completion handler that it will call when appropriate (e.g., generally in URLSessionDidFinishEventsForBackgroundURLSession).
How did you "terminate the app"? If you manually kill it (by double tapping on home button, holding down on icon for running app, and then hitting the little red "x"), that will not only terminate the app, but it will stop background sessions, too. Alternatively, if the app crashes or if it is simply jettisoned because foreground apps needed more memory, the background session will continue.
Personally, whenever I want to test background operation after app terminates, I have code in my app to crash (deference nil pointer, like Apple did in their WWDC video introduction to NSURLSession). Clearly you'd never do that in a production app, but it's hard to simulate the app being jettisoned due to memory constraints, so deliberately crashing is a fine proxy for that scenario.
i insert this new lines of code:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
BLog();
NSInteger errorReasonNum = [[error.userInfo objectForKey:#"NSURLErrorBackgroundTaskCancelledReasonKey"] integerValue];
if([error.userInfo objectForKey:#"NSURLErrorBackgroundTaskCancelledReasonKey"] &&
(errorReasonNum == NSURLErrorCancelledReasonUserForceQuitApplication ||
errorReasonNum == NSURLErrorCancelledReasonBackgroundUpdatesDisabled))
{
NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
if (resumeData) {
// resume
NSURL *downloadURL = [NSURL URLWithString:DownloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
if (!self.downloadTask) {
self.downloadTask = [self.session downloadTaskWithRequest:request];
}
[self.downloadTask resume];
if (!_session){
[[_session downloadTaskWithResumeData:resumeData]resume];
}
}
}
}
It catch NSURLErrorCancelledReasonUserForceQuitApplication but when the application try to [[_session downloadTaskWithResumeData:resumeData]resume]
reenter again in:
(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
and give me again -999 error.
I use this configuration
- (NSURLSession *)backgroundSession
{
/*
Using disptach_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:#"com.example.apple-samplecode.SimpleBackgroundTransfer.BackgroundSession"];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session;
}
let me explain what i mean with "terminate the app" (in ios8):
double tap on home button
swipe on my open app.
app disappear from open app list
relaunch app.
When i reopen the app i enter into callback with error
The operation couldn't be completed. (NSURLErrorDomain error -999.)
There is something that i can't understand. This behaviour make me crazy! :-(

NSURLSessionTask never calls back after timeout when using background configuration

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

Newsstand and network errors

Guys I'm working on the Newsstand stuff now. I'm trying to handle network errors.
What you see on the image below is my simple log ("Percentage: %i" is inside connection:didWriteData:totalBytesWritten:expectedTotalBytes:).
My problem is depicted in the last 3 lines of code.
What I've done in this lines:
After that line I've switched on the airplane mode (simulated network error)
I've received connection:didWriteData:totalBytesWritten:expectedTotalBytes: with totalBytesWritten equal to expectedTotalBytes
I've received connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *)destinationURL.
After that:
Hooray, I've just finished downloading my .zip, I can unpack it, announce the status to my view and so on... :(
My question is what's going on?
I have implemented connection:didFailWithError: but it's not invoked.
I was trying to grab the totalBytesWritten in last invoked didWriteData: and compare it to real file size in DidFinishDownloading:
I have stripped all my project away just to make sure that its not related to my whole design.
I'm thinking about combination of NSTimer and NKIssueContentStatusAvailable to check the real download status.
It's all hacky. Isn't it?
Update:
Reproduced on iOS 6 and 7 with XCode 5
All NewsstandKit methods invoked on the main thread
Same thing when simulating offline mode with Charles proxy (app in foreground)
It's not an issue anymore when switching to Airplane, but still can reproduce the issue when throttling on Charles proxy.
I ended up with this solution (checking if connection:didWriteData:... is telling the truth in connectionDidFinishDownloading:destinationURL:):
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long)expectedTotalBytes
{
...
self.declaredSizeOfDownloadedFile = expectedTotalBytes;
}
And:
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL
{
NSDictionary* fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:destinationURL.absoluteString error:nil];
NSNumber* destinationFileSize = [fileAttributes objectForKey:NSFileSize];
if (destinationFileSize.intValue != self.declaredSizeOfDownloadedFile)
{
NSError* error = ...;
[self connection:connection didFailWithError:error];
self.declaredSizeOfDownloadedFile = 0;
return;
}
...
}

Resources