I am sending a DataTask to some website and in URL I have redirection to localhost (https://...&redirect_uri=http://localhost...). Overall in that call I am getting about 5 redirections, where localhost is probably third and after that redirection localhost keeps in its URL important string that I want to take, so I decided to prevent redirections, when URL will start with localhost and its further specific URL:
extension MyClass: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
if response.url?.absoluteString.hasPrefix("http://localhost/bar?code=") ?? false {
completionHandler(nil)
}
completionHandler(request)
}
and I set my URLSession object's delegate as that class:
private var session: URLSession { URLSession(configuration: .default, delegate: self, delegateQueue: nil) } // `self` is MyClass
After sending request mentioned in the beginning of the question Xcode throws that error:
Task <UIID>.<1> finished with error [-1004] Error Domain=NSURLErrorDomain Code=-1004 "Could not connect to the server." UserInfo={NSUnderlyingError=0x600000cc46f0 {Error Domain=kCFErrorDomainCFNetwork Code=-1004 "(null)" UserInfo={_kCFStreamErrorCodeKey=61, _kCFStreamErrorDomainKey=1}}, NSErrorFailingURLStringKey=http://localhost/bar?code=[code], NSErrorFailingURLKey=http://localhost/bar?code=[code], _kCFStreamErrorDomainKey=1, _kCFStreamErrorCodeKey=61, NSLocalizedDescription=Could not connect to the server.}
As You see, that delegate method isn't called (I've additionally checked that with breakpoints). I found that some other methods of that delegate doesn't fire when I am using closure on dataTask(with:), but it doesn't apply on redirection handling method (I've checked that, I have deleted closures and it still isn't called).
Additional info:
Request is created like here:
let url = URL(string: https://...)
var request = URLRequest(url: url)
request.addValue([someValue], forHTTPHeaderField: "User-Agent")
And this is my task:
let task = session.dataTask(with: request) // Request created like above
task.resume()
And will Combine methods (.dataTaskPubliser()) apply to that delegate?
I found that's probably a bug (reported). Combine's publisher of URLSessionDataTask just doesn't call some methods of the delegate. Switched for now to classic Foundation's implementation.
Related
I was implementing an upload task using URLSession Uploadtask with the below code:
lazy var urlSession = URLSession(
configuration: .background(withIdentifier: "com.test.xxxxx"),
delegate: self,
delegateQueue: .main
)
var uploadTask = URLSessionUploadTask()
/// Calling uploadtask using 'fileURL' of the asset
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "PUT"
uploadTask = urlSession.uploadTask(with: request, fromFile: fileURL)
uploadTask.resume()
And uploading works as expected, my concern is if I want to use resume data whenever user removes the app from multitask window or any error happens in between uploading a file, how can i achieve it using below delegate method, this delegate method is firing for me, but we don't have any methods to use resume data like func downloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask for upload task or is it not possible for upload task, please guide me on this. Thank you.
func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
// Here comes when background upload failed with errors
// Such as app closed from the tray ,switched off ,crash occured, etc.
// Then, handle with resumeData
os_log("Download error: %#", type: .error, String(describing: error))
} else {
// Here comes when background upload completed with no error
os_log("Task finished: %#", type: .info, task)
}
}
Edit: I can't see anything related to resume data for upload task in Apple doc also.
I'm relatively new to swift and I'm having issues trying to call a function in a view controller from a delegate I have defined. How can I call the function in my view controller from this delegate? This is a mixed project consisting of mostly Objective-C code with only one Swift controller. The function is inside of the Swift controller. Below is the delegate class:
class DelegateToHandle302:NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
//convert to https
let http = request.url!
var comps = URLComponents(url: http, resolvingAgainstBaseURL: false)!
comps.scheme = "https"
let httpsUrl = comps.url!
ViewControllerFunction(url: httpsUrl)
}
I get an error Use of unresolved identifier 'ViewControllerFunction'. I've tried creating an instance of the view controller but don't think that's the correct way to do it as this view controller also has an audio player (it also didn't work).
Here is where I call the delegate from a function inside the view controller:
let urlString = "https://urlthatredirects.com"
let config = URLSessionConfiguration.default
let url = URL(string: urlString)
//set delegate value equal to SessionDelegate to handle 302 redirect
let delegate = DelegateToHandle302()
//establish url session
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
//set task with url
let dataTask = session.dataTask(with: url!)
//init call
dataTask.resume()
I'm following part of an example on how to get the final URL from a redirection (https://gist.github.com/mgersty/b565ba4c9e9422637f15f52a5317f07e). My view controllers "header" is:
#objc class AudioPlayerController: UIViewController{........}
I hope I've provided enough info to allow anyone to assist me in figuring out what I'm doing wrong. The only thing I need to do is call that function and pass the redirection URL to it.
I'm a bit confused about what you're trying to do, and why you're trying to call a delegate method back into the VC rather than using the completion handler; but I think you've got your delegate pattern back-to-front. I'm assuming the idea is:
the view controller instiagtes the URL session
the url sessions passes of the result of the URLSession to the DelegateToHandle302 to process
DelegateToHandle302 then tries to run a method back in the view controller that launched it.
If this is the case you actually need the VC to be the delegate of the DelegateToHandle302 class, not the other way around.
So within your view controller
let handlerFor302 = DelegateToHandle302()
handlerFor302.delegate = self.
let session = URLSession(configuration: config, delegate: handlerFor302, delegateQueue: nil
//etc... as before
Create a protocol for the delegate to adopt, which defines the desired function
protocol URLProcessor {
func ViewControllerFunction(url: URL)
}
The adopt the protocol in your view controller and implement the method
extension MyViewController: URLProcessor {
func ViewControllerFunction(url: URL) { .... do whatever ...}
and then use the delegate with the protocol method in your DelegateToHandle302
class DelegateToHandle302:NSObject, URLSessionTaskDelegate {
weak var delegate: URLProcessor?
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
//Process the output
delegate?.ViewControllerFunction(url: httpsUrl)
}
I have an app that needs to download a file which may be rather large (perhaps as large as 20 MB). I've been reading up on URLSession downloadTasks and how they work when the app goes to the background or is terminated by iOS. I'd like for the download to continue and from what I've read, that's possible. I found a blog post here that discusses this topic in some detail.
Based on what I've read, I first created a download manager class that looks like this:
class DownloadManager : NSObject, URLSessionDownloadDelegate, URLSessionTaskDelegate {
static var shared = DownloadManager()
var backgroundSessionCompletionHandler: (() -> Void)?
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
private override init() {
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let completionHandler = self.backgroundSessionCompletionHandler {
self.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let sessionId = session.configuration.identifier {
log.info("Download task finished for session ID: \(sessionId), task ID: \(downloadTask.taskIdentifier); file was downloaded to \(location)")
do {
// just for testing purposes
try FileManager.default.removeItem(at: location)
print("Deleted downloaded file from \(location)")
} catch {
print(error)
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
let progressPercentage = progress * 100
print("Download with task identifier: \(downloadTask.taskIdentifier) is \(progressPercentage)% complete...")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Task failed with error: \(error)")
} else {
print("Task completed successfully.")
}
}
}
I also add this method in my AppDelegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
DownloadManager.shared.backgroundSessionCompletionHandler = completionHandler
// if the app gets terminated, I need to reconstruct the URLSessionConfiguration and the URLSession in order to "re-connect" to the previous URLSession instance and process the completed download tasks
// for now, I'm just putting the app in the background (not terminating it) so I've commented out the lines below
//let config = URLSessionConfiguration.background(withIdentifier: identifier)
//let session = URLSession(configuration: config, delegate: DownloadManager.shared, delegateQueue: OperationQueue.main)
// since my app hasn't been terminated, my existing URLSession should still be around and doesn't need to be re-created
let session = DownloadManager.shared.session
session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
// downloadTasks = [URLSessionDownloadTask]
print("There are \(downloadTasks.count) download tasks associated with this session.")
for downloadTask in downloadTasks {
print("downloadTask.taskIdentifier = \(downloadTask.taskIdentifier)")
}
}
}
Finally, I start my test download like this:
let session = DownloadManager.shared.session
// this is a 100MB PDF file that I'm using for testing
let testUrl = URL(string: "https://scholar.princeton.edu/sites/default/files/oversize_pdf_test_0.pdf")!
let task = session.downloadTask(with: testUrl)
// I think I'll ultimately need to persist the session ID, task ID and a file path for use in the delegate methods once the download has completed
task.resume()
When I run this code and start my download, I see the delegate methods being called but I also see a message that says:
A background URLSession with identifier com.example.testapp.background already exists!
I think this is happening because of the following call in application:handleEventsForBackgroundURLSession:completionHandler:
let session = DownloadManager.shared.session
The getter for the session property in my DownloadManager class (which I took directly from the blog post cited previously) is always trying to create a new URLSession using the background configuration. As I understand it, if my app had been terminated, then this would be the appropriate behavior to "reconnect" to the original URLSession. But since may app is not being terminated but rather just going to the background, when the call to application:handleEventsForBackgroundURLSession:completionHandler: happens, I should be referencing the existing instance of URLSession. At least I think that's what the problem is. Can anyone clarify this behavior for me? Thanks!
Your problem is that you are creating a new session every time you reference the session variable:
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
Instead, keep the session as an instance variable, and just get it:
class DownloadManager:NSObject {
static var shared = DownloadManager()
var delegate = DownloadManagerSessionDelegate()
var session:URLSession
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
override init() {
session = URLSession(configuration: config, delegate: delegate, delegateQueue: OperationQueue())
super.init()
}
}
class DownloadManagerSessionDelegate: NSObject, URLSessionDelegate {
// implement here
}
When I do this in a playground, it shows that repeated calls give the same session, and no error:
The session doesn't live in-process, it's part of the OS. You're incrementing reference count every time you access your session variable as written, which causes the error.
UPDATE
I've found that if I run the server and then the macOS app and leave it for 40 seconds (so the server has sent 40 "a" characters, one each second) then eventually the didReceive response delegate is called, and the didReceive data delegate then starts getting called with every new bit of data. This leads to logging like this in the console of the macOS app:
URLAuthenticationChallenge
Got response: <NSHTTPURLResponse: 0x6080000385c0> { URL: https://localhost:10443/sub } { status code: 200, headers {
"Content-Type" = "text/plain; charset=utf-8";
Date = "Thu, 03 Nov 2016 16:51:28 GMT";
Vary = "Accept-Encoding";
} }
Received data: Optional("{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n{\"Data\":\"a\"}\n")
Received data: Optional("{\"Data\":\"a\"}\n")
Received data: Optional("{\"Data\":\"a\"}\n")
Received data: Optional("{\"Data\":\"a\"}\n")
Received data: Optional("{\"Data\":\"a\"}\n")
Received data: Optional("{\"Data\":\"a\"}\n")
Received data: Optional("{\"Data\":\"a\"}\n")
...
which suggests that there's some buffering going on somewhere.
I've been testing out how URLSession works with HTTP/2 connections and I've run into some issues.
I've got an incredibly simple macOS app here: https://github.com/hamchapman/http2-barebones-mac-app although the whole code for it is basically just this:
class ViewController: NSViewController, URLSessionDelegate, URLSessionDataDelegate {
override func viewDidLoad() {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "localhost"
urlComponents.port = 10443
guard let url = urlComponents.url else {
print("Bad URL, try again")
return
}
var request = URLRequest(url: url.appendingPathComponent("/sub"))
request.httpMethod = "SUB"
request.timeoutInterval = REALLY_LONG_TIME
let sessionConfiguration = URLSessionConfiguration.ephemeral
sessionConfiguration.timeoutIntervalForResource = REALLY_LONG_TIME
sessionConfiguration.timeoutIntervalForRequest = REALLY_LONG_TIME
let session = Foundation.URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil)
let task: URLSessionDataTask = session.dataTask(with: request)
task.resume()
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
print("Got response: \(response)")
completionHandler(.allow)
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let dataString = String(data: data, encoding: .utf8)
print("Received data: \(dataString)")
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("Error: \(error)")
}
// So it works with self-signed certs (we don't care about TLS etc in this example)
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.previousFailureCount == 0 else {
challenge.sender?.cancel(challenge)
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let allowAllCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
completionHandler(.useCredential, allowAllCredential)
}
}
You can see that the http method being used is SUB. This is designed to be a method that you use if you want to subscribe to a given resource, which in my simple example is at path /sub. This should in theory be able to make use of HTTP/2 streaming to send new data over to the macOS app's connection, when the server has new data to send.
Here is the very basic (Go) app that I've been using as the server: https://github.com/hamchapman/http2-barebones-server (the readme has instructions on how to run it).
It's basically setup to accept a SUB request at /sub and send back a 200 OK immediately, and then every second it sends "a" as a bit of data.
The problem I'm facing is that as far as the Go server is concerned, the connection is being made fine. However, the URLSessionDelegate gets called with the expected URLAuthenticationChallenge (the server only allows encrypted connections), but the URLSessionDataDelegate methods that get called when a response is received and when data is received are never called.
You can verify that the server is working as expected by running it and then using the following curl command:
curl --http2 -k -v -X SUB https://localhost:10443/sub
(you might need to download the latest version of curl - see here for info: https://simonecarletti.com/blog/2016/01/http2-curl-macosx/)
I've also verified that the data is actually being received by the connection made in the macOS app (using Wireshark), but the delegate never gets called.
Does anyone know why this might be happening? Is data getting buffered somewhere? Is HTTP/2 support not fully there in URLSession?
It's because the first 512 bytes are buffered: https://forums.developer.apple.com/thread/64875
For a screen scraping project I'm using NSURLSession in Swift to read an HTML page. But already the start fails because the returned page gives a redirection to a new webpage and my code doesn't follow that. I thought redirection would work by default, if no delegate is set for the session. But neither case does the redirection. Here's my test project:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSURLSessionTaskDelegate {
#IBOutlet weak var window: NSWindow!
func URLSession(session: NSURLSession, task: NSURLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse,
newRequest request: NSURLRequest, completionHandler: (NSURLRequest!) -> Void) {
completionHandler(request);
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
var url = NSURL(string: "https://banking.dkb.de");
let defaultConfigObject = NSURLSessionConfiguration.defaultSessionConfiguration();
let session = NSURLSession(configuration: defaultConfigObject, delegate: self, delegateQueue: nil);
let task = session.dataTaskWithURL(url!, completionHandler: { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
let text = NSString(data: data, encoding: NSUTF8StringEncoding);
// text contains here: <head><meta HTTP-EQUIV="REFRESH" content="0; url=/dkb/"></head>
if var document = NSXMLDocument(data: data, options: Int(NSXMLDocumentTidyHTML), error: nil) {
if let forms = document.nodesForXPath("//form[#name='login']", error: nil) where forms.count > 0 {
let form = forms[0] as! NSXMLNode;
}
}
});
task.resume();
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
What do I have to do to make redirection work here?
Update:
On further investigation I found out that it must have to do with the result from the server. For instance using https://www.google.com indeed triggers the redirection delegate. However, since any browser can handle also redirection from the bank address, there must be a different approach in place to properly handle that and I'd like to learn how.
NSURLSession supports 302 redirect and https://www.google.com uses it.
On the other hand, https://banking.dkb.de/ uses meta tag as described. It returns 200(OK) as status code so you must handle it reading meta tag.