URLSessionDataDelegate not being called using http2 connection - swift

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

Related

Am unable to receive HTTP responses with UIViewController set as URLSessionDelegate

Wed 5/18 Additional Info added at Step 5
I am able to create a URLSesion, build a request with a file to upload and successfully call it from my app. On my server side, the proper script is called, uploaded file is saved, etc,. However, I am not receiving the HTTP responses, data, etc.
Actually had this working without the delegate, when the HTTP response functions were within the task itself. But am now trying to expand functionality and am missing something while trying implement the delegate.
The trimmed code is below, and it all works, with the exception of setting up UIViewController as the URLSession delegate. Just trying to figure out why my UIViewController is not receiving the HTTP responses.
Below is the code for:
UIViewController
Class which creates the upload session (UploadService)
Extension for
UIViewController which I want to use to process the responses
How the previous task looked, when it worked. Before I tried to implement the delegate.
Used print to confirm that my UIViewConroller is the delegate, yet it still receives no HTTP response, data, or error messages
UIViewController
class UploadInv : UIViewController {
var xFile : XFile?
...create UI....
let uploadService = UploadService()
lazy var uploadSession: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
}()
override func viewWillAppear(_ animated: Bool) {
...
}
override func viewDidLoad() {
super.viewDidLoad()
uploadService.uploadSession = uploadSession
... code the lays out all buttons, labels, etc...
}
#objc func buttonAction(sender: UIButton!) {
guard let theButton = sender else { return}
let myTag = theButton.tag
switch myTag {
//button to start upload
case ButtType.up.rawValue:
uploadService.start(upFile: xFile!, script: "uploadOrig.pl", upLoadInvClass: self)
uploadService.task?.resume()
//button to select file to upload
case ButtType.file.rawValue:
... file xFile with file info
}
}
UploadService
class UploadService {
var uploadSession : URLSession!
var task: URLSessionUploadTask?
func start(upFile: XFile, script: String, upLoadInvClass: UploadInv) {
var request = upFile.makeUrlReq(upFile: upFile, script: script)
task = uploadSession.uploadTask(with: request, from: request.httpBody! )
print("\(uploadSession.delegate)")
task?.resume()
}
}
extension
extension UploadInv: UIDocumentPickerDelegate, URLSessionDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
... file xFile info for upload ....
... http request created ....
}
// Below are the three simple functions which I would handle
// responses the server, but these never seem to get called.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let err = error {
print("Error: \(err.localizedDescription)")
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: (URLSession.ResponseDisposition) -> Void) {
print("didReceive response")
completionHandler(URLSession.ResponseDisposition.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
print("didReceive data")
if let responseText = String(data: data, encoding: .utf8) {
print(responseText)
}
}
}
Pre-Delegate model which worked
class UploadService {
var uploadSession = URLSession.shared
func start(upFile: XFile, script: String, upLoadInvClass: UploadInv) {
var request = upFile.makeUrlReq(upFile: upFile, script: script)
uploadSession.uploadTask(with: request, from: request.httpBody )
{ (data, response, error) in
if let response = response {
upLoadInvClass.upResp(resp: response)
}
if let error = error {
upLoadInvClass.upErr(error: error)
}
if let data = data {
upLoadInvClass.upData(data: data)
}
}.resume()
}
}
Step 5:
task = uploadSession.uploadTask(with: request, from: request.httpBody! )
print("\(uploadSession.delegate)")
task?.resume()
For other newbies also stuck on this, it turns out there's more than one delegate to look at. There are:
URLSessionTaskDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate, and more. So obviously I was using the wrong one, might have been fell trap to "autocomplete." Nevertheless, I have to make sure I read more documentation on the subject.
Thanks to Scott who "passively/aggressively" gave me the answer, here, while still allowing me to "think." I mean that as a compliment. He told me to add the line:
assert(uploadSession.delegate! is URLSessionDataDelegate)

URLSession omits delegate's redirection management method

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.

URLSession downloadTask behavior when running in the background?

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.

NSURLSession didReceiveData not being triggered

I'm having a problem detecting when data is being received using NSURLSession. The equivalent code with NSURLConnection does work, but that's not included here.
In this example, I'm doing a request to google.com. The completionHandler works and "complete" is printed (also the data, etc if you change the code).
However didReceiveData isn't triggered and "received data" is never printed.
I've been through the docs and done a ton of searching and I think this looks right, but I can't seem to get it to work. Definitely would appreciate any help with this.
(I need to use didReceiveData because I'm going to parsing a streaming json api.)
Thanks!
import UIKit
class ViewController: UIViewController, NSURLSessionDelegate, NSURLSessionDataDelegate, NSURLSessionTaskDelegate {
override func viewDidAppear(animated: Bool) {
let session = NSURLSession.sharedSession()
var task = session.dataTaskWithURL(NSURL(string: "https://google.com")!, completionHandler: { (data, response, error) -> Void in
print("complete")
})
task.resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
print("received data")
}
}
There were two issues.
When the session is created, you must define a delegate. That was the main reason didReceiveData wasn't being called.
The second issue is that if you use a completionHandler block, then all the delegates functions are bypassed. In the code for NSURlSession, it says
extension NSURLSession {
/*
* data task convenience methods. These methods create tasks that
* bypass the normal delegate calls for response and data delivery,
* and provide a simple cancelable asynchronous interface to receiving
* data. Errors will be returned in the NSURLErrorDomain,
* see <Foundation/NSURLError.h>. The delegate, if any, will still be
* called for authentication challenges.
*/
You must implement each delegate function you need to check for completion, errors, etc.
The updated code is below:
import UIKit
class ViewController: UIViewController, NSURLSessionDelegate {
override func viewDidAppear(animated: Bool) {
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
var task = session.dataTaskWithURL(NSURL(string: "https://google.com")!)
task.resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
print("received data")
}
}
You can get data in completionHandler only. Why do you want to use didReceiveData?
Below code will show you how you can get the received data
override func viewDidAppear(animated: Bool) {
let session = NSURLSession.sharedSession()
var task = session.dataTaskWithURL(NSURL(string: "https://google.com")!, completionHandler: { (data, response, error) -> Void in
if NSJSONSerialization.isValidJSONObject(data){
if let jsonParam = try? NSJSONSerialization.dataWithJSONObject(dictData, options: []){
print("Result Data : \(jsonParam)")
}
}
})
task.resume()
}

NSURLSession does not follow redirection

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.