Prevent NSURLSessionDataTask from repeatedly firing request - ios

I am using a NSURLSessionDataTask to get JSON data from a device that opens an adhoc-WiFi which is joined by an iPhone. (The connection is running via TCP).
The device is pretty slow - it takes roundabout 5sec to send the particular response.
After firing the request via dataTask.resume() my app is just waiting for the completionHandler to be called.
Unfortunately the request is fired again and again (each after a delay of ~ 1 sec). I guess this is initiated by the dataTask or NSURLSession. I can monitor the repeated requests when debugging the device.
Here's the code I'm using:
let URL = NSURL(string: "<myURL>")!
let configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
configuration.HTTPShouldUsePipelining = false
configuration.HTTPMaximumConnectionsPerHost = 1
configuration.allowsCellularAccess = false
configuration.timeoutIntervalForRequest = 30
let session = NSURLSession(configuration: configuration)
let dataTask = session.dataTaskWithURL(URL) {
// the request gets fired repeatedly before this completionHandler is called!
(data, response, error) -> Void in
...
}
dataTask.resume()
Does anyone know how to prevent iOS from re-firing these requests?

Related

URLSession request: request Code=-1009 "(null)" UserInfo={_NSURLErrorNWPathKey=unsatisfied (Denied over cellular interface)

Test is performed on iPhone 13 Pro 15.3.1, with Xcode 13.2.1. I have narrow down the issue as below two tests. The test code below just send a simple https request.
I have allowed the network access for the test app on both Wifi and cell. And I'm a paid developer.
All following two tests are passed in simulator but failed on iPhone, so it means something is wrong in my iPhone side, find a post said China brand iPhone may have this issue, still digging...orz
The error exists with WiFi and Cellular. URLSession request is denied on both Wi-Fi interface and cellular interface.
NSURLErrorNWPathKey=unsatisfied...
1st test:
The test code is provided in Apple doc (https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations).
func testDownloadWebData() {
// Create an expectation for a background download task.
let expectation = XCTestExpectation(description: "Download apple.com home page")
// Create a URL for a web page to be downloaded.
let url = URL(string: "https://apple.com")!
// Create a background task to download the web page.
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, _) in
// Make sure we downloaded some data.
XCTAssertNotNil(data, "No data was downloaded.")
// Fulfill the expectation to indicate that the background task has finished successfully.
expectation.fulfill()
}
// Start the download task.
dataTask.resume()
// Wait until the expectation is fulfilled, with a timeout of 10 seconds.
wait(for: [expectation], timeout: 10.0)
}
2nd attempt:
I also try my version, but get the same result.
func testDownloadWebData() {
// Create an expectation for a background download task.
let expectation = expectation(description: "Download apple.com home page")
// Create a URL for a web page to be downloaded.
let url = URL(string: "https://apple.com")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
let session = URLSession(configuration: config)
// Create a background task to download the web page.
let dataTask = session.dataTask(with: request) { (data, _, error) in
// put a breakpoint here, no triggered.
guard error == nil else {
print(error!.localizedDescription)
return
}
// Make sure we downloaded some data.
XCTAssertNotNil(data, "No data was downloaded.")
// Fulfill the expectation to indicate that the background task has finished successfully.
expectation.fulfill()
}
// Start the download task.
dataTask.resume()
// Wait until the expectation is fulfilled, with a timeout of 10 seconds.
wait(for: [expectation], timeout: 10.0)
}

iOS background download not respecting httpMaximumConnectionsPerHost

I am on iOS 14.2...
I have a URLSession that i configure that way:
private lazy var backgroundURLSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "background-download")
config.sessionSendsLaunchEvents = true
config.httpMaximumConnectionsPerHost = 2
config.timeoutIntervalForRequest = 120
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
I give it like 100 URLs to download
let downloadTask = session.downloadTask(with: offlineTile.url)
downloadTask.resume()
and even with httpMaximumConnectionsPerHost = 2 the server gets ALL requests at ONCE... ?!?
What could I be doing wrong
One more note: We have a Varnish Cache in the background... and noticed that the behavior differs, if Varnish is set to pipe (no caching)
In our case it was the load balancer of our server that did not properly to it's https termination, which caused iOS to send all requests at basically once.

NSURLSession, after the data task is converted to download task, it can't download in background

If I run the following code and let the app in background, the download is still continuing. Finally, when the download is finished, I can get the right callback.
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)
let backgroundSession = NSURLSession(configuration: configuration, delegate: self.delegate, delegateQueue: nil)
let url = NSURLRequest(URL: NSURL(string: data[1])!)
let downloadTask = backgroundSession.downloadTaskWithRequest(url)
downloadTask.resume()
But I have a requirement, that is I have to judge what the server returns to me, if it is a json, I don't do the download, so I want to get the response header first, then if it needs to download, I change the data task to download task, so I did as the following code
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)
let backgroundSession = NSURLSession(configuration: configuration, delegate: self.delegate, delegateQueue: nil)
let url = NSURLRequest(URL: NSURL(string: data[1])!)
//I change the downloadTaskWithRequest to dataTaskWithRequest
let downloadTask = backgroundSession.dataTaskWithRequest(url)
downloadTask.resume()
Then I can get the response header in the callback, and if it needs to download file, I can change the data task to download task, as following
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
if let response = response as? NSHTTPURLResponse {
let contentType = response.allHeaderFields["Content-Type"] as! String
if contentType == "image/jpeg" {
//change the data task to download task
completionHandler(.BecomeDownload)
return
}
}
completionHandler(.Allow)
}
So far so good. When I run the app in the foreground, the effect is like what I thought. But after the app runs in background, the download is stoped, then when I open the app, the console says "Lost connection to background transfer service".
I thought Apple is so smart, he gives us many useful callbacks, but now, I didn't know where I am wrong, and I also see the source code about the AFNetworking and Alamofire, but I didn't find the referring thing.
I also think it is a common requirement, but I can't find any helpful information on the internet, it is too odd.
So hope you can help me out, thanks a billion.
Enable Background Mode in
Xcode->Target->Capabilities->On Background Mode and select the option Background Fetch.
The main issue I see is that you're calling the completionHandler twice. You need to return out of your content-type conditional like so:
if contentType == "image/jpeg" {
//change the data task to download task
completionHandler(.BecomeDownload)
return
}
Otherwise it appears that you are using the logic correctly. Hope that helps.
The problem is evident from your own answer. It's not a bug, you simply couldn't use data tasks for background transfers just download tasks.
Here is the correct full answer.

iOS NSURLSession with Wi-Fi Assist turned on results in crashing app

I'm performing an HTTP request using iOS's NSURLSession. My code looks like this:
let session = NSURLSession.sharedSession()
let url = NSURL(string:"www.example.com")
guard let url = url else {
return
}
let request = NSURLRequest(URL: url)
var task = session.dataTaskWithRequest(request){
(data, response, error) -> Void in
//do stuff with the data
}
task.resume()
(sorry if my code isn't 100% correct I just typed it real quickly inside my browser, you get the idea)
When Wi-Fi Assist is turned off it works fine, but when Wi-Fi Assist is turned on the app crashes.
I found this but the discussion never got an answer.
Apart from the fact that I want to fix the problem I am very curious WHY this is happening.

How to switch views in the handler of an NSURLSession Request

I have a Login View Controller, and an Other View Controller. What I'd like to do is: when the user hits login, it sends their credentials to the remote server. The remote server returns a response indicating whether the credentials were good or not, and if they were good, the app redirects to the Other View Controller.
The code below crashes at the call to .performSegueWithIdentifier.
The crash gives an error code of EXC_BAD_ACCESS(code=1, address=0xbbadbeef)
Question: what is the swifty way of doing this?
var request = NSMutableURLRequest(URL: NSURL(string: "http://url.to/my/login/handler")!)
var session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
//user initialized earlier
bodyData = "email=\(user.username)&password=\(user.password)"
request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
// check that log in was successful by looking in 'response' arg
// if login was successful
self.performSegueWithIdentifier("SegueToOtherController", sender: self)
}
task.resume()
}
If it's crashing, you should share the details the crash in order to identify why. Likely problems include that it didn't find a segue of that identifier as the "storyboard id" from the current view controller to the next scene. But it's impossible to say without details on the precise error.
Having said that, there is another problem here: The completion block may not run on the main thread, but all UI updates must happen on the main thread. So make sure to dispatch that back to the main queue, e.g.
let request = NSMutableURLRequest(URL: NSURL(string: "http://url.to/my/login/handler")!)
let session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
//user initialized earlier
bodyData = "email=\(user.username)&password=\(user.password)"
request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
let task = session.dataTaskWithRequest(request) {data, response, error in
// check that log in was successful by looking in 'response' arg
// if login was successful
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("SegueToOtherController", sender: self)
}
}
task.resume()
Note, I also changed all of those var references to let (as a general rule, use let wherever possible). Also, I haven't tackled it here, but you really should be percent escaping the username and password properties. If, for example, the password included any reserved characters like + or &, this would fail. There are lots of ways of doing that, e.g. something like the method discussed here: https://stackoverflow.com/a/26317562/1271826 or https://stackoverflow.com/a/25154803/1271826.

Resources