Swift 5 Background upload not receiving http response headers and failing silently - ios

I've been working on a background file upload to Google Cloud with an iOS app using SwiftUI with Swift 5. I've gotten it to work successfully when everything is configured correctly, but it fails silently and reports success when I intentionally have things incorrectly configured (Incorrect address, bad auth token, etc.). It will even give me data sent updates for the entire upload even though it's not being received.
My question is: How do I get the correct HTTP response headers to tell me that the upload has failed?
Here is the code creating the background url session:
let configuration = URLSessionConfiguration.background(withIdentifier: Constants.URL_SESSION_IDENTIFIER)
configuration.sessionSendsLaunchEvents = true
configuration.sharedContainerIdentifier = "my-suite-name"
self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
Here is the code to upload the file:
func uploadFile(fileUrl: URL, token: String, contentType: String, uploadUrl: URL) {
print("Uploading video")
var urlRequest = URLRequest(url: uploadUrl)
urlRequest.setValue(contentType, forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.httpMethod = "POST"
urlSession.uploadTask(with: urlRequest, fromFile: fileUrl).resume()
}
On the class (self) that is the delegate, I've implemented this protocol URLSessionTaskDelegate and added these two functions:
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { }
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { }
These all work when it's configured correctly, but continue to work even when it's not configured correctly...I can't figure out a way to get them to let me know that the upload isn't actually being received or being rejected. If I try a similar upload with Postman it will immediately respond back with an error and stop running the upload, while this will still run.
I've tried implementing a couple other related protocols with some of their related functions but had no success. For example I found the URLSessionDataDelegate protocol has a function:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive: URLResponse) { }
Which is the only one in the family of URLSession Delegates that receives http headers that I can find, but it doesn't get triggered when running the uploads.
Thanks for your help and let me know if there is more information needed.

Related

Can't able to catch error when turn off network connection at the time of downloading a file

I am trying to download .mp3 file from server using using URLSession. It also works in background. Here is the variables i have declared.
var urlSession: URLSession?
var sessionTask: URLSessionDownloadTask?
var resumeData: Data?
This is how i initialised the URLSession.
let config = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
config.networkServiceType = .background
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
To download i am using URLSessionDownloadTask. I have also implemented pause/resume functionality. Here is the code.
func startDownload(with url: String) {
sessionTask = urlSession?.downloadTask(with: URL(string: url)!)
sessionTask?.resume()
}
func cancelDownload() {
sessionTask?.cancel()
}
func pauseDownload() {
sessionTask?.cancel(byProducingResumeData: { (data) in
self.resumeData = data
})
}
func resumeDownload(url: String) {
if let resumeData = self.resumeData {
sessionTask = urlSession?.downloadTask(withResumeData: resumeData)
} else {
sessionTask = urlSession?.downloadTask(with: URL(string: url)!)
}
sessionTask?.resume()
}
As i have enabled download in background, to receive success, failure and progress of download i have confirmed URLSessionDownloadDelegate methods.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
Everything seems fine when i download a file even in background. I have got the success, failure and progress of the download in delegate methods.
But the problem is if i turn off network connection at the middle of downloading a file, i get some error logs in console. But none of the above delegate methods are called. Here is the logs
Task <0B34AE9E-0A46-4E78-8C90-7353CDC34929>.<10> finished with error [-1020] Error Domain=NSURLErrorDomain Code=-1020 "A data connection is not currently allowed." UserInfo={_kCFStreamErrorCodeKey=50, NSUnderlyingError=0x2817b40c0 {Error Domain=kCFErrorDomainCFNetwork Code=-1020 "(null)" UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <0B34AE9E-0A46-4E78-8C90-7353CDC34929>.<10>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <0B34AE9E-0A46-4E78-8C90-7353CDC34929>.<10>"
), NSLocalizedDescription=A data connection is not currently allowed.
How can i get the error response to show the user that you have lost your network connection?
I have searched for it but didn't find the solution. Pardon me if it's a duplicate question. Thanks.
To get the error returned, you need to use the alternative functions of downloadTask that have a completionHandler`
self.urlSsn?.downloadTask(with: MyURLRequest, completionHandler: { data, response, error -> Void in
if let error = error {
print("\(error.localizedDescription)")
// This should print "A data connection is not currently allowed."
}
}
A similar is available for downloadTask(withResumeData) as well.
If you want to compare specific error codes (as error code -1020 in your example). You can do the following:
if let error = error {
let e = error! as NSError
switch e.code {
case NSURLErrorNotConnectedToInternet:
print("No internet")
default:
print("Some other error")
}
}
More info here and here

URLSession Credentials Caching Allowing Authentication with Incorrect Credentials

I am trying to communicate with my company's API in my iOS app. I am using the standard URLSession.
The API will load balance and redirect to a different server automatically, so I've implemented the URLSessionDelegate and URLSessionTaskDelegate methods which handle the redirects.
When I initially login I will get redirected from http://our.api.com to http://our1.api.com or some other version of the API with a different server number. The first time I authenticate with http://our1.api.com it will honor the passed in Authorization header and challenged URLCredential. But if I try to authenticate against the same API again with known bad credentials, the old URLCredential is used and I am able to get into the API when I should not be able to.
Is there a way to force URLSession to never use the cached URLCredential, or otherwise clear out the cached URLCredentials?
Creating the URLSession
let config = URLSessionConfiguration.ephemeral
config.httpAdditionalHeaders = ["Accept":"application/xml",
"Accept-Language":"en",
"Content-Type":"application/xml"]
config.requestCachePolicy = .reloadIgnoringLocalCacheData
config.urlCache = nil
self.urlSession = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
Calling to the API
var request = URLRequest(url: thePreRedirectedUrl)
request.httpMethod = "GET"
request.addValue("Basic username:password", forHTTPHeaderField: "Authorization")
let task = urlSession?.dataTask(with: request, completionHandler: { (data, response, error) in
// pass endpoint results to completion block
completionBlock(data, response, error)
})
// run the task
if let task = task {
task.resume()
}
URLSessionDelegate and URLSessionTaskDelegate
extension ApiManager: URLSessionDelegate, URLSessionTaskDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.previousFailureCount == 0 {
completionHandler(.useCredential, URLCredential(user: username, password: password, persistence: .none))
} else {
completionHandler(.performDefaultHandling, nil)
}
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: #escaping (URLRequest?) -> Void) {
var newRequest = URLRequest(url: request.url!)
newRequest.addValue("Basic username:password", forHTTPHeaderField: "Authorization")
newRequest.httpMethod = task.originalRequest?.httpMethod
newRequest.httpBody = task.originalRequest?.httpBody
completionHandler(newRequest)
}
}
The most reliable way is to delete the credential from the user's (macOS) or app's (iOS) keychain.
See Updating and Deleting Keychain Items on Apple's developer website for details, but basically:
NSDictionary *matchingDictionary = #{
kSecClass: kSecClassInternetPassword,
kSecAttrSecurityDomain: #"example.com" // <-- This may not be quite the
// right format for the domain.
};
SecItemDelete((__bridge CFDictionaryRef) matchingDictionary);

Response Data via URLSessionUploadTask

I am writing simple handler for communication with REST API on server (currently local). Everything goes well so far with downloading and uploading data from/to server.
What I am trying to achieve now is to be able to handle JSON response returned by server after uploading data. This message is something like this:
{"message":"Record successfully added.","recordID":30}
Important is for me the recordID, because I need to assign it to relevant NSManagedObject. I use delegation attitude instead of completionHandler so I would be able to manage progress of the upload. Appropriate delegate class implements these protocols with all methods:
class ConstructoHTTPHelper:NSObject, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate { ... }
Here comes the issue because as far as I create upload task with something like this:
let config = URLSessionConfiguration.default
self.session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main) //URLSession(configuration: config)
var request:URLRequest = URLRequest(url:address)
request.httpMethod = "POST"
let data = // creation of data ...
let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("uploadData")
do {
try data.write(to: fileURL)
} catch {
// handling error
}
self.sessionUploadTask = self.session?.uploadTask(with: request, fromFile: fileURL)
self.sessionUploadTask!.resume()
The delegate func for handling data:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {}
returned by server is never called.
What is strange to me is that when I use completion Handler like the one below, It prints the JSON well:
self.sessionUploadTask = self.session?.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
print(NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!)
})
So it looks to me that uploadTask is limited in this way. Any suggestions?
Thanks
I probably found the answer, just add this to URLSession:dataTask:didReceiveResponse:completionHandler: delegate method.
completionHandler(URLSession.ResponseDisposition.allow)
I found solution in this thread.
try this!, get a NSMutableData as buffer like this globally
fileprivate var buffer:NSMutableData = NSMutableData()
and in your URLSession delegate method add,
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let _ = error {
print(error!.localizedDescription)
}else {
// do your parsing with buffer here.
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
buffer.append(data)
}

didReceive Challenge Authentication Method Not Called

Following is my code . I am trying to use didReceive challenge method for authentication. Apple documents says that If a session task requires authentication, and there are no valid credentials available, then 'didReceive challenge' method is called. but in this case it is not being called. Any advice will be appreciated. Thanks :)
func getServerResponse(){
var request=URLRequest(url: URL(string: "http://dev.example.com/Api/Account")!)
let configuration=URLSessionConfiguration.default
request.httpMethod="GET"
let task=URLSession.init(configuration: configuration).dataTask(with: request, completionHandler: {(data,response,error) -> Void in
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary {
print("Result-->\(jsonResult)")
print((response as! HTTPURLResponse).statusCode)
}
} catch let error as NSError {
print(error.localizedDescription)
}
})
task.resume()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let crdential = URLCredential.init(user:"userName", password: "password", persistence: URLCredential.Persistence.none)
completionHandler(URLSession.AuthChallengeDisposition.useCredential, crdential)
}
The delegate is not being called because you have not set the delegate in the first place......
Use this method:
let task = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
Also, set the URLSessionDataDelegate and conform the protocol:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Swift.Void)
Instead of using dataTask with completion handler because when you use that method it does not call any of the delegate methods.
Would like to supplement the existing answer by mentioning that Apple provide an excellent article on this. The article includes sample code. See Handling an Authentication Challenge.
Also, to mention, Apple - in both in their docs and developer forums - strongly recommend the delegate approach to handling basic authentication challenges. However! it’s worth noting that not all REST API’s issue a challenge. For example, some API’s provide a default level of access to anonymous users. They do not issue the challenge.
So, having implemented delegates correctly, if the callbacks are not executing, it’s worth checking the REST API documents on basic auth.
It maybe that there is no choice but to delegate handling and pass the appropriate headers instead:
var request = URLRequest(url: url)
request.setValue(basicAuthHeader, forHTTPHeaderField: "Authorization”)
and to provide the headers:
// Set the security header
private var credentials: String {
return "\(username):\(password)"
}
private var basicAuthHeader: String {
let data = credentials.data(using: String.Encoding.utf8)!
let encoded = data.base64EncodedString()
return "Basic \(encoded)"
}

Background Network Call from Background Push

I have an application that communicates with the Apple Watch. When the main iOS app gets a remote notification, it fires off a network request using URLSession and then also sends the result to the watch.
The issue is that when the iOS app is in the background and the push notification is received, the network call is never made. I can see, via breakpoints, that the code is executed but not the code when data is received.
I do have the content-available flag set and I have also tried using the URLSessionConfiguration.background without success.
var config:URLSessionConfiguration!
var session:URLSession?
private func configureSession()
{
self.config = URLSessionConfiguration.background(withIdentifier: "backgroundFetch")
//self.config = URLSessionConfiguration.default
self.config.sessionSendsLaunchEvents = true
self.config.timeoutIntervalForRequest = 15.0
self.config.timeoutIntervalForResource = 15.0
self.session = URLSession(configuration: self.config, delegate: self, delegateQueue: nil)
}
You can see above that I tried it with both default and the background mode.
func getCoordinatesForID(_ trackID:String, withPassCode passCode:String, andMyID myID:String)
{
let url = self.WEB_DOMAIN+"getcoord"
let hash = SecurityModel.sha256(myID+trackID+passCode)!
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded;charset=utf-8", forHTTPHeaderField: "Content-Type")
let postDataStr = "auth=\(hash)&id=\(myID)&trk=\(trackID)&code=\(passCode)"
let requestBodyData = postDataStr.data(using: String.Encoding.utf8)
request.httpBody = requestBodyData
let postDataTask = self.session!.dataTask(with: request)
postDataTask.taskDescription = "getCoordinates"
postDataTask.resume()
}
The above function is called when in the background but this is where things stop. Nothing is ever received. Works fine in foreground.
I do implement the below is neither is called when in the background.
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
and
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
Update:
Per the comment from Edward I did update capabilities to include "background fetch" although the documentation on downloading after a notification did not indicate this was necessary. That seems to have worked but only in part and so a problem persists.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
The above is getting called but is throwing the error: "Lost connection to background transfer service". Happens 100% of the time when in the background state but not the foreground.
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
The above which I need and is called first before the method above it, is not called at all. So overall things have progressed a bit further but still don't work in the same way as it does when in the foreground.
Solution:
The solution was to remove the timeoutIntervalForRequest and timeoutIntervalforResource Despite the fact that I never waited the 15 seconds I set before the issue resulted, removing these allowed the process to work correctly in the background. Additionally I am able to use the standard URLSessionConfiguration.default rather than the .background.
So in the end adding the "background fetch" capability and removing the timeouts resolved the issue.
I found after more tests with the ideas above that things were not working consistently. The issue is that I was using URLSessionDataTask which, as it turns out, is not support in the background. Per Apple's documentation:
Data tasks request a resource, returning the server’s response as one
or more NSData objects in memory. They are supported in default,
ephemeral, and shared sessions, but are not supported in background
sessions.
To solve this problem I had to create a URLSessionDownloadTaskand use its associated delegate
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
With the above the network calls worked properly when the app was in the background.

Resources