NSURLSession Redirection handler different behaviour on WatchOS - ios

I am working on updating an old WatchOS app to support WatchOS 4.0. The app makes some network requests, one of which responds with a 302 redirect status code and corresponding Location header. I do not want to follow the redirection, and the 302 response headers contains all the info I need for this particular request.
I am using NSURLSession and have implemented the delegate method - URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:.
The delegate method is called as expected, and inside it I call completionHandler(nil) to tell the OS not to follow the redirect. All is fine up to this point.
The NSURLSessionTask completion handler at this point typically gets called immediately after this with a request cancelled error, and that's fine too.
Here's the strange part. When the Apple Watch is paired with an iPhone, I get the behaviour described above. However when I put the iPhone into Aeroplane Mode to simulate the Watch being out of range, with the network requests being made directly over wifi or cellular, I get a different behaviour.
The redirection delegate method is still called, as expected, however the NSURLSessionTask completion handler does not get called immediately. Rather, it gets called some time later with a -1001 Request Timed Out error.
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// Odd behaviour described above here
}];
[task resume];
The delegate redirection method:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
completionHandler(nil);
}
This has been frustratingly difficult to debug to this point as I cannot debug through Xcode and have not been able to find a way to proxy the watch network requests.
Has anyone experienced this behaviour, or have any clues on why it's behaving differently? Is this a WatchOS defect?

Related

NSURLSessionDownloadTask switching from http to https

I have an AFHTTPSessionManager created NSURLSessionDownloadTask being used to download a video within an app that sometimes when initialized with an http:80 url will convert it to https:443. This is happening before any connection attempt is being made (I added a custom HTTP protocol class via NSURLSessionConfiguration to the session in order to log when the connection is being made).
By the time the request makes it to the
-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
delegate method of my CustomHTTPProtocolDelegate class, the request has been changed to https.
App Transport Security is disabled (NSAllowsArbitraryLoads=true) and this behavior seems to be associated with a particular http-only server (other http-only server have no issue, and the connection is made as http on port 80).
Any idea of what could be going on? Anything else I could do to debug?
Here is how the download task is being created (including the debug custom protocol class):
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
config.protocolClasses = #[[CustomHTTPProtocol class]];
AFHTTPSessionManager *session = [[AFHTTPSessionManager manager] initWithSessionConfiguration:config];
self.downloadTask = [session downloadTaskWithRequest:request progress:&progress destination:^NSURL *(NSURL *targetPath, NSURLResponse *response){
...
[UPDATE]
This issue is causing us a number of headaches, so to help facilitate troubleshooting, I created a small test project to help narrow in on the problem. My simple project does two things, loads a url into a UIWebView and downloads a file using NSURLSessionDownloadTask. The urls for these actions follow this pattern:
WebView URL: https://console.company.com/home.html
Download URL: http://data.company.com/file.txt
And those hostnames resolve to different IPs (different servers).
If I download the file before navigating the webview, then everything is fine, but if the webview loads its URL first, then the download URL will be switched to HTTPS automatically and the initial request for data will fail. One thought we had was that once iOS opens a TLS tunnel for the HTTPS connection that the webview is creating, that it tries to use that same tunnel for all subsequent *.company.com connections. Or at the very least, it assumes all *.company.com connections must also be TLS.
Figured it out. Both servers were sending a HSTS header for all subdomains. Because the networking layer under NSURLSession observes this header, the calls to the HTTP server were being upgraded to HTTPS prior to leaving the client.

'NSData dataWithContentsOfURL:' memory leak iOS 9.x?

My code is the following:
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *theURLString = #"http://website.com/musicFile";
NSData *theData = [NSData dataWithContentsOfURL:[NSURL URLWithString:theURLString]];
}
There is nothing special at all. I am not even using the background thread.
Here is the behavior I get on iOS 8.x (and the behavior that I expect to get):
So, NSData is fully released and all of the occupied memory is back.
However, iOS 9.x surprised me a lot:
My questions are:
Approximately 100 MB are gone for nothing in iOS 9.x. How can I get them back? Are there any workarounds?
iOS 8.x has occupied 136.2 MB at max, while iOS 9.x used 225.9 MB at max. Why is this happening?
What is going on in iOS 9.x?
UPDATE #1:
I have also tried using NSURLSession 'dataTaskWithURL:completionHandler:' (thanks to #CouchDeveloper). This reduces the leak, but doesn't fully solve the problem (this time both iOS 8.x and iOS 9.x).
I used the code below:
NSURLSession *theURLSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDataTask *theURLSessionDataTask = [theURLSession dataTaskWithURL:[NSURL URLWithString:theURLString]
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
{
NSLog(#"done");
}];
[theURLSessionDataTask resume];
As you can see, 30 MB are still lost.
UPDATE #2:
The above tests where done using Xcode simulator.
However, I have also decided to test on actual iOS 9.2 iPhone 4S (as recommended by #Sohil R. Memon).
The results of 'NSData dataWithContentsOfURL:' are below:
The results of using 'NSURLSession dataTaskWithURL:completionHandler:' are below:
It looks like 'NSData dataWithContentsOfURL:' works perfectly on actual device, while 'NSURLSession dataTaskWithURL:completionHandler:' -- doesn't.
However, does anyone know solutions which show identical information on BOTH actual device AND Xcode simulator?
Approximately 100 MB are gone for nothing in iOS 9.x. How can I get them back? Are there any workarounds?
For a couple of reasons, we should use NSURLSession to download data from a web service. So, this is not a workaround, but the correct approach.
What is going on in iOS 9.x?
I have no idea - possibly cached data, network buffers, or some other issues. But this is irrelevant - you should try the correct approach with NSURLSession.
From the docs:
IMPORTANT
Do not use this synchronous method to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated.
Instead, for non-file URLs, consider using the dataTaskWithURL:completionHandler: method of the NSURLSession class. See URL Session Programming Guide for details.
Edit:
Those "reasons" are:
NSURLSession is specifically designed to load remote resources.
NSURLSession methods are asynchronous which is crucial for methods which complete only after a perceivable duration (it will not block the calling thread).
A session can handle authentication by means of a default method or with a custom delegate.
Session tasks can be cancelled.
Here is also an answer which helped me. The answer states to use [NSData dataWithContentsOfURL:url options:0 error:&error]; instead.
Hope this helps

Background task execution is not working as expected

I have a requirement to make an web-service call up on tapping actionable buttons from the Remote notification.
On the Remote notification, we have two buttons "Details" and "Fetch". "Fetch" is a background action (Activation Mode = UIUserNotificationActivationModeBackground) and will send some data with out launching the app.
On tapping "Fetch", i made a web-service call to send some details which i got from remote notification.
I have used NSURLSession to send those data to server.
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"com.applewatch.sample"];
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
NSURLSessionUploadTask *dataTask = [self.session uploadTaskWithRequest:mutableURLRequest fromData:nil];
[dataTask resume];
This works fine when the app is in active state but not in the suspended mode. What i meant to say here is, Once we get the Remote notification and if act after 1 hour on the notification, the service request is not going through.
I suspect, the service request is not going through because the app is in suspended mode/termination mode.
Could some help me how to make service calls in the background.
Note: I have not enabled "Background Fetch" options in info.plist.
Let me know if need to enable and implement the service calls in the UIApplication delegate
application:performFetchWithCompletionHandler:
I'm not being able to recall or find again where I read this information before, but try using
- uploadTaskWithRequest:fromFile:
I believe it's the only upload method that will work in background

NSURLSession didReceiveChallenge only called once

I'm using NSURLSession dataTaskWithRequest:completionHandler: to make some requests to my server. I'm using a self-signed certificate, so when the delegate's URLSession:didReceiveChallenge:completionHandler: gets called, I can do my internal checks to verify it everything is fine.
This all works great on the first request I send. After I call the completion handler with NSURLSessionAuthChallengeUseCredential, then the next requests never call URLSession:didReceiveChallenge:completionHandler:.
For reasons I won't go into in this post, I would really like URLSession:didReceiveChallenge:completionHandler: to be called each time, so that I can do my own checking. I'm assuming NSURLSession is somehow caching the certificate and keeping it in some sort of "valid certificates" list so it's not doing this check each time. I want to clear this cache so that I can make my own check each time. If I restart the App, then URLSession:didReceiveChallenge:completionHandler: does get called once more.
I have tried setting the Credential Storage to nil with no success:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.URLCredentialStorage = nil;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
[[session dataTaskWithRequest:request
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
...
}] resume];
Any ideas how to accomplish this?
Thank you!
For someone who tried everything here can be problem in the backend. So this problem was fixed by disabling ssl session caching in nginx server.

How to work with large file uploads in ios?

My app requires upload of video files from users phone which will then be processed on server.
THe problem is the size of the file can go to 200 MB plus and the user won't keep the application open to wait for the file to upload. Since apple doesn't allow apps to run in background for more than a limited time. How can I ensure that my files are uploaded. I am using afnetworking to set up an upload task as given by ios 7 library.
Please if anyone can point me in the right direction or have any solution it would be greatly appreciated. I have banged my head on this for too long. Thanks.
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
[manager setTaskDidSendBodyDataBlock:^(NSURLSession *session,NSURLSessionTask *task ,int64_t bytesSent, int64_t totalBytesSent,int64_t totalBytesExpectedToSend){
CGFloat progress = ((CGFloat)totalBytesSent / (CGFloat)sensize);
NSLog(#"Uploading files %lld -- > %lld",totalBytesSent,totalBytesExpectedToSend);
[self.delegate showingProgress:progress forIndex:ind];
}];
dataTask = [manager uploadTaskWithStreamedRequest:request progress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
if (error) {
NSLog(#"Error: %#", error);
} else {
}
}];
My request is a normal multipart form request.
Use:
NSURLSessionConfiguration:backgroundSessionConfiguration:
instead of
NSURLSessionConfiguration:defaultSessionConfiguration
From the NSURLSessionConfiguration:backgroundSessionConfiguration: documentation:
Upload and download tasks in background sessions are performed by an external daemon instead of by the app itself. As a result, the transfers continue in the background even if the app is suspended, exits, or crashes.
So in your case, change:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
to:
NSString *appID = [[NSBundle mainBundle] bundleIdentifier];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:appID];
Implementing application:handleEventsForBackgroundURLSession:completionHandler: on your app delegate will allow your app to be woken up (ie. un-suspended or un-terminated in background mode) when an upload has completed (whether it has completed successfully or not).
Don't get confused with Background Fetching. You don't need it. Background Fetching simply wakes you app up to periodically give your app the chance to fetch small amounts of content regularly. It may however, be useful for restarting failed "background-mode" uploads periodically.
You should use a background session configuration instead if a default session configuration. This ensures that your data transfer will continue in the background once the user has exited your app.
Of course, this is correct as long as the user has background fetching enabled for your app in the Settings app of the device.
Be sure to enable the Background fetch capability on your project settings:
(source: migueldiazrubio.com)
(source: migueldiazrubio.com)
Then implement the application:handleEventsForBackgroundURLSession:completionHandler: in your App Delegate to be informed when the data transfer ends and do whatever you need to do (UI update…) inside your app with the received/sent file. Don't forget to call the completionHandler to inform the system that you have ended your tasks. iOS then will take a screenshot of the active screen of your app and update the one in the iOS 7 multitasking screen.

Resources