Response Data via URLSessionUploadTask - ios

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)
}

Related

AVAssetDownloadTask method 'didFinishDownloadingTo' is not called in background mode with low disk space

I create a background session like this:
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: backgroundSessionId)
backgroundConfiguration.isDiscretionary = false
backgroundConfiguration.sessionSendsLaunchEvents = true backgroundConfiguration.shouldUseExtendedBackgroundIdleMode = true
privateQueue = OperationQueue()
privateQueue.maxConcurrentOperationCount = 1
assetDownloadSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: privateQueue)
Also create and run a task:
let task = assetDownloadSession.makeAssetDownloadTask(asset: urlAsset, assetTitle: title, assetArtworkData: nil, options: nil)
task.resume()
But if I have less than 500MB of disk space on my real device the application is restarted and the following method is called:
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
But the next method is not called:
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL)
The file is not deleted from the device, how can I find out where it is locally and how to delete it? The system does not do this automatically.
Checked many times on real iphone 6s, iOS 14.0.1 with logs and Mac OS console application.
If the memory is more than 500 MB, then everything works correctly and the method didCompleteWithError is called
You can use AVAggregateAssetDownloadTask to get a location of your downloading media in URLSession:aggregateAssetDownloadTask:willDownloadToURL:e.g.:
var assetDownloadURLSession: AVAssetDownloadURLSession!
var task = AVAggregateAssetDownloadTask?
var downloadURL: URL?
func download(asset: AVURLAsset)
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "AAPL-Identifier")
assetDownloadURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
task = assetDownloadURLSession.aggregateAssetDownloadTask(with: asset, ...)
task?.resume()
...
}
func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, willDownloadTo location: URL) {
downloadURL = location
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let url = downloadURL {
do {
try FileManager.default.removeItem(at: url)
}
catch {
print(error)
}
}
}
How to work with aggregate tasks you can look at Apple's sample project: https://developer.apple.com/documentation/avfoundation/media_playback_and_selection/using_avfoundation_to_play_and_persist_http_live_streams

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);

download file with urlsesssion

I'm trying to download using urlsession background session this is my main function
func startfresh() {
session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
let url = URL(string: "https://nava.ir/wp-content/uploads/2018/08/Gholamreza-Sanatgar-Dorooghe-Sefid-128.mp3")
task = session.downloadTask(with: url!)
task.resume()
}
and my didcompletewitherror
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
let err = error as NSError?
let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
print("anotherone")
let newtask = session.downloadTask(withResumeData: resumeData!)
newtask.resume()
}
else {
print("hichi")
}
}
but when I close the app when the download is still on progress and relaunch it again and press start download it starts 2 tasks resume previous one and start a new one I want to just resume the previous one with resume data what should I do to just trigger did complete with error method.
What you are seeing is kind of "expected" and you have to design your software to handle it. Actually, there are some more things you should consider. I've investigated and put down at the next as an answer. (NSURLSessionDownloadTask move temporary file)
A sample project is also available.

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)"
}

NSURLProtocol. requestIsCacheEquivalent never called

I'm not sure what the deal is here, but the function:
class func requestIsCacheEquivalent(a: NSURLRequest, toRequest b: NSURLRequest) -> Bool
is never called within my NSURLProtocol subclass. I've even seen cases of the cache being used (verified by using a network proxy and seeing no calls being made) but this method just never gets invoked. I'm at a loss for why this is.
The problem I'm trying to solve is that I have requests that I'd like to cache data for, but these requests have a signature parameter that's different for each one (kind of like a nonce). This makes it so the cache keys are not the same despite the data being equivalent.
To go into explicit detail:
I fire a request with a custom signature (like this:
www.example.com?param1=1&param2=2&signature=1abcdefabc312093)
The request comes back with an Etag
The Etag is supposed to be managed by the NSURLCache but since it thinks that a different request (www.example.com?param1=1&param2=2&signature=1abdabcda3359809823) is being made it doesn't bother.
I thought that using NSURLProtocol would solve all my problems since Apple's docs say:
class func requestIsCacheEquivalent(_ aRequest: NSURLRequest,
toRequest bRequest: NSURLRequest) -> Bool
YES if aRequest and bRequest are equivalent for cache purposes, NO
otherwise. Requests are considered equivalent for cache purposes if
and only if they would be handled by the same protocol and that
protocol declares them equivalent after performing
implementation-specific checks.
Sadly, the function is never called. I don't know what the problem could be...
class WWURLProtocol : NSURLProtocol, NSURLSessionDataDelegate {
var dataTask: NSURLSessionDataTask?
var session: NSURLSession!
var trueRequest: NSURLRequest!
private lazy var netOpsQueue: NSOperationQueue! = NSOperationQueue()
private lazy var delegateOpsQueue: NSOperationQueue! = NSOperationQueue()
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
println("can init with request called")
return true
}
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
println("canonical request for request called")
return request
}
override class func requestIsCacheEquivalent(a: NSURLRequest, toRequest b: NSURLRequest) -> Bool {
// never ever called?!?
let cacheKeyA = a.allHTTPHeaderFields?["CacheKey"] as? String
let cacheKeyB = b.allHTTPHeaderFields?["CacheKey"] as? String
println("request is cache equivalent? \(cacheKeyA) == \(cacheKeyB)")
return cacheKeyA == cacheKeyB
}
override func startLoading() {
println("start loading")
let sharedSession = NSURLSession.sharedSession()
let config = sharedSession.configuration
config.URLCache = NSURLCache.sharedURLCache()
self.session = NSURLSession(configuration: config, delegate: self, delegateQueue: self.delegateOpsQueue)
dataTask = session.dataTaskWithRequest(request, nil)
dataTask?.resume()
}
override func stopLoading() {
println("stop loading")
dataTask?.cancel()
}
//SessionDelegate
func URLSession(session: NSURLSession, didBecomeInvalidWithError error: NSError?) {
println("did become invalid with error")
client?.URLProtocol(self, didFailWithError: error!)
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
println("did complete with error")
if error == nil {
client?.URLProtocolDidFinishLoading(self)
} else {
client?.URLProtocol(self, didFailWithError: error!)
}
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
println("did receive response")
client?.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .Allowed)
completionHandler(.Allow)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
println("did receive data called")
client?.URLProtocol(self, didLoadData: data)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, willCacheResponse proposedResponse: NSCachedURLResponse, completionHandler: (NSCachedURLResponse!) -> Void) {
println("will cache response called")
client?.URLProtocol(self, cachedResponseIsValid: proposedResponse)
completionHandler(proposedResponse)
}
I registered the protocol in my app delegate as follows:
NSURLProtocol.registerClass(WWURLProtocol.self)
I trigger the protocol as follows:
#IBAction func requestData(endpointString: String) {
let url = NSURL(string: endpointString)
let request = NSMutableURLRequest(URL: url!)
var cacheKey = endpointString
request.setValue("\(endpointString)", forHTTPHeaderField: "CacheKey")
request.cachePolicy = .UseProtocolCachePolicy
NSURLConnection.sendAsynchronousRequest(request, queue: netOpsQueue) { (response, data, error) -> Void in
if data != nil {
println("succeeded with data:\(NSString(data: data, encoding: NSUTF8StringEncoding)))")
}
}
}
I think that in practice, the loading system just uses the canonicalized URL for cache purposes, and does a straight string comparison. I'm not certain, though. Try appending your nonce when you canonicalize it, in some form that is easily removable/detectable (in case it is already there).
Your code seems all right.You just follow documents of Apple about URLProtocol.You could try to use URLSession for NSURLConnection is deprecated in newer iOS version.Good luck.

Resources