How to open HTTP stream on iOS using ndjson - ios

Does anyone have experience opening HTTP stream on iOS? I have tried multiple solutions without any luck (examples bellow).
For better context, here's example of endpoint that will stream values (as ndjson) upon opening connection:
GET /v2/path/{id}
Accept: application/x-ndjson
Attempt #1:
Issue: The completion handler is never called
let keyID = try keyAdapter.getKeyID(for: .signHash)
let url = baseURL.appendingPathComponent("/v2/path/\(keyID)")
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
urlRequest.setValue("application/x-ndjson", forHTTPHeaderField: "Accept")
session.dataTask(with: urlRequest) { data, response, error in
// This never gets called.
// I would expect that the completion is called every time backend emits new value.
}.resume()
Attempt #2:
Issue: Debugger displays this message: Connection 0: encountered error(12:1)
private var stream: URLSessionStreamTask? = nil
func startStream() {
let keyID = try keyAdapter.getKeyID(for: .signHash)
let url = baseURL.appendingPathComponent("/v2/path/\(keyID)")
let stream = session.streamTask(withHostName: url, port: 443)
// Not sure how to set headers.
// Header needs to be set so backend knows client wants to connect a stream.
self.stream = stream
stream.startSecureConnection()
startRead(stream: stream)
}
private func startRead(stream: URLSessionStreamTask) {
stream.readData(ofMinLength: 1, maxLength: 4096, timeout: 120.0) { data, endOfFile, error in
if let error = error {
Logger.shared.log(level: .error, "Reading data from stream failed with error: \(error.localizedDescription)")
} else if let data = data {
Logger.shared.log(level: .error, "Received data from stream (\(data.count)B)")
if !endOfFile {
self.startRead(stream: stream)
} else {
Logger.shared.log(level: .info, "End of file")
}
} else {
Logger.shared.log(level: .error, "Reading stream endup in unspecified state (both data and error are nil).")
}
}
}
Does anyone have experience with this? How can I keep HTTP connection open and listen to a new values that backend is streaming?

iOS can connect to HTTP stream using now deprecated API URLConnection. The API was deprecated in iOS 9, however it's still available for use (and will be in iOS 16 - tested).
First you need to create URLRequest and setup the NSURLConnection:
let url = URL(string: "\(baseURL)/v2/path/\(keyID)")!
var urlRequest = URLRequest(url: url)
urlRequest.setValue("application/x-ndjson", forHTTPHeaderField: "Accept")
let connnection = NSURLConnection(request: urlRequest, delegate: self, startImmediately: true)
connnection?.start()
Notice that the argument for delegate in the code above is of type Any which doesn't help to figure out what protocol(s) to implement. There are two - NSURLConnectionDelegate and NSURLConnectionDataDelegate.
Let's receive data:
public func connection(_ connection: NSURLConnection, didReceive data: Data) {
let string = String(data: data, encoding: .utf8)
Logger.shared.log(level: .debug, "didReceive data:\n\(string ?? "N/A")")
}
Then implement a method for catching errors:
public func connection(_ connection: NSURLConnection, didFailWithError error: Error) {
Logger.shared.log(level: .debug, "didFailWithError: \(error)")
}
And if you have custom SSL pinning, then:
public func connection(_ connection: NSURLConnection, willSendRequestFor challenge: URLAuthenticationChallenge) {
guard let certificate = certificate, let identity = identity else {
Logger.shared.log(level: .info, "No credentials set. Using default handling. (certificate and/or identity are nil)")
challenge.sender?.performDefaultHandling?(for: challenge)
return
}
let credential = URLCredential(identity: identity, certificates: [certificate], persistence: .forSession)
challenge.sender?.use(credential, for: challenge)
}
There is not much info on the internet, so hopefully it will save someone days of trial and error.

Related

How to define a fallback case if a remote GET request fails?

I recently started with iOS development, and I'm currently working on adding new functionality to an existing app. For this feature I need to obtain a JSON file from a web server. However, if the server is unreachable (no internet/server unavailable/etc), a local JSON needs to be used instead.
In my current implementation I tried using a do catch block, but if there's no internet connection, the app just hangs instead of going to the catch block. JSON parsing and local data reading seem to work fine, the problem is likely in the GET method, as I tried to define a callback to return the JSON data as a separate variable, but I'm not sure if that's the correct way.
What is the best way to handle this scenario?
let url = URL(string: "https://jsontestlocation.com") // test JSON
do {
// make a get request, get the result as a callback
let _: () = getRemoteJson(requestUrl: url!, requestType: "GET") {
remoteJson in
performOnMainThread {
self.delegate.value?.didReceiveJson(.success(self.parseJson(jsonData: remoteJson!)!))
}
}
}
catch {
let localFile = readLocalFile(forName: "local_json_file")
let localJson = parseJson(jsonData: localFile!)
if let localJson = localJson {
self.delegate.value?.didReceiveJson(.success(localJson))
}
}
getRemoteJson() implementation:
private func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
// Method which returns a JSON questionnaire from a remote API
var request = URLRequest(url: requestUrl) // create the request
request.httpMethod = requestType
// make the request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// check if there is any error
if let error = error {
print("GET request error: \(error)")
}
// print the HTTP response
if let response = response as? HTTPURLResponse {
print("GET request status code: \(response.statusCode)")
}
guard let data = data else {return} // return nil if no data
completion(data) // return
}
task.resume() // resumes the task, if suspended
}
parseJson() implementation:
private func parseJson(jsonData: Data) -> JsonType? {
// Method definition
do {
let decodedData = try JSONDecoder().decode(JsonType.self, from: jsonData)
return decodedData
} catch {
print(error)
}
return nil
}
If you don't have to use complex logic with reachability, error handling, request retry etc. just return nil in your completion in case of data task, HTTP and No data errors:
func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
var request = URLRequest(url: requestUrl)
request.httpMethod = requestType
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Task error
guard error == nil else {
print("GET request error: \(error!)")
completion(nil)
return
}
// HTTP error
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("GET request failed: \(response!.description)")
completion(nil)
return
}
// No data
guard let data = data else {
completion(nil)
return
}
completion(data)
}
task.resume()
}
let url = URL(string: "https://jsontestlocation.com")!
getRemoteJson(requestUrl: url, requestType: "GET") { remoteJson in
if let json = remoteJson {
print(json)
...
}
else {
print("Request failed")
...
}
}
func NetworkCheck() -> Bool {
var isReachable = false
let reachability = Reachability()
print(reachability.status)
if reachability.isOnline {
isReachable = true
// True, when on wifi or on cellular network.
}
else
{
// "Sorry! Internet Connection appears to be offline
}
return isReachable
}
Call NetworkCheck() before your API request. If It returns false, read your local json file. if true do remote API call.
Incase after remote API call, any failure check with HTTP header response code.
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
}
I think you need to stop the request from hanging when it’s waiting for a response. The app might be running on a poor connection and be able to get some but not all the data in which case you likely want to failover to the local JSON.
I think you can roughly use what you have but add a timeout configuration on the URLSession as described here: https://stackoverflow.com/a/23428960/312910

Google calendar API delete / update recurring event returns 412 on URLSession

I am trying to delete a parent recurring event (so the parent and all the instances will be deleted).
When using URLSession with manually constructed URLRequest the request returns 412 error code. I do not provide If-Match header and there have been no changes whatsoever to the event from the creation to its deletion.
Executing the request with Postman or Paw the request succeeds with status code 204 which is the desired result.
I construct the request as such:
func delete(eventWithId eventId: String, token: String) {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
guard var URL = URL(string: "https://www.googleapis.com/calendar/v3/calendars/primary/events/\(eventId)") else {return}
var request = URLRequest(url: URL)
request.httpMethod = "DELETE"
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
if (error == nil) {
let statusCode = (response as! HTTPURLResponse).statusCode // <--- returning status code 412
}
else {
print("URL Session Task Failed: %#", error!.localizedDescription);
}
})
task.resume()
session.finishTasksAndInvalidate()
}
The response body is:
{
"error":{
"errors":[
{
"domain":"global",
"reason":"conditionNotMet",
"message":"Precondition Failed",
"locationType":"header",
"location":"If-Match"
}
],
"code":412,
"message":"Precondition Failed"
}
}
Weird thing. I do the same DELETE request using Postman and PAW and it succeeds, returning status code 204. Also my delete request seems to be working fine in single events or single occurrences of the recurring event.
Anyone with experience on the matter?
So what was actually happening, was that there was a "If-None-Match" header added that wouldn't show up when I was debugging the request object. I had to audit the request using (Charles) proxy to see it.
Overriding this header field seems to have resolved this issue.

URLSession doesn't pass 'Authorization' key in header swift 4

I am trying to pass authorization key in header of a URLRequest. But at the server end the key is not received. The same API when called from postman working fine. Any other key in the header is working fine, even authorizations key is visible at server end.
Here is my code:
let headers = [
"authorization": "token abcd"
]
var request = URLRequest.init(url: NSURL(string:
"http://127.0.0.1:7000/api/channels?filter=contributed")! as URL)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = headers
let session = URLSession.init(configuration: config)
let dataTask = session.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error ?? "")
} else {
let httpResponse = response as? HTTPURLResponse
print(httpResponse ?? "")
}
})
As you can see, I tried to set the token in both session config and request but none is working.
This seems to be working:
// Set the security header
private var credentials: String {
return "\(participantId):\(password)"
}
private var basicAuthHeader: String {
return "Basic \(credentials)"
}
func getSettings(participantId: Int, password: String) -> Bool {
self.participantId = participantId
self.password = password
let path = "/settings/\(participantId)"
guard let url = URL(string: "\(BASE_URL)\(path)") else {
Log.e("Invalid URL string, could not convert to URL")
return false
}
var urlRequest = URLRequest(url: url)
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.setValue(basicAuthHeader, forHTTPHeaderField: "Authorization")
urlRequest.setValue(APP_FILE_NAME, forHTTPHeaderField: "User-Agent")
// This is a synchronous wrapper extension around URLSession.dataTask()
let (data, response, error) = URLSession.shared.synchronousDataTask(with: urlRequest)
// Process the result...
}
Note: code written by my coworker. Thanks John!
Looks like the problem is that you are modifying Authorization header using httpAdditionalHeaders which is something you should not do.
From the Doc
An NSURLSession object is designed to handle various aspects of the HTTP protocol for you. As a result, you should not modify the following headers:
Authorization,
Connection,
Host,
Proxy-Authenticate,
Proxy-Authorization,
WWW-Authenticate
Removing the line config.httpAdditionalHeaders = headers
should fix the issue.
If you want token to be hardcoded, I guess it has to be like this:
urlRequest.httpMethod = "GET"
urlRequest.setValue("Token <Your Token>", forHTTPHeaderField: "Authorization")
I found the same thing: setting the header field Authorization just didn't do the trick.
Here's the solution I settled on (which works well):
I added the URLSessionDelegate protocol to my current class. This unfortunately means inheriting from NSObject.
Then, when defining my URLSession, I set its delegate to 'self'.
Finally, I provide an authentication challenge handler.
In code, this all looks like:
public class SomeHTTPTask: NSObject, URLSessionDelegate {
public init() {
... initialize variables ...
super.init()
... now you are free to call methods on self ...
}
public func httpTask(withURL url: URL) {
let request = URLRequest(url: url)
... set up request ...
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: request) {data, response, error in
... now you have a result ...
}
}
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let user = Credentials.sharedInstance.userId, let password = Credentials.sharedInstance.password else {
completionHandler(.performDefaultHandling, nil)
return
}
let userCredential = URLCredential(user: user,
password: password,
persistence: .permanent)
completionHandler(.useCredential, userCredential)
}
}
Hopefully, the bits and pieces are self-explanatory. It's just an authentication challenge handler that provides credentials, if it can. The underlying URLSession will deal with the details, wither it's an NTLM or Basic auth, those sorts of things.
In the end, this seems a solid solution. At least, it worked for me.
Here's a nice reference document from Apple if you like reading that kind of thing.

what am I doing wrong Swift 3 http request and response

I'm having big problems in a project I'm currently working with.
I've been reading about URLSession various places but all of them seem to be outdated and refers to NSURLSession I thought that they would be fairly similar and they probably are but for a newbie like me I'm lost. what I do is not working and I do not like solutions I find because they all do their work in a controller..
http://swiftdeveloperblog.com/image-upload-with-progress-bar-example-in-swift/
this one for instance. I'm using the PHP script but wanted to make a networking layer I could invoke and use at will. but I'm lacking a good resource from where I could learn about how to use this api.
every place I find is similar to the link above or older. the few newer seem to also follow the pattern without really explaining how to use this api.
at the same time I'm new to the delegate pattern in fact I only know that it is something that is heavily used in this Api but I have no IDEA how or why.
Basically I need help finding my way to solve this problem here:
I've tried to do something like this:
public class NetworkPostRequestor:NSObject,NetworkPostRequestingProtocol,URLSessionTaskDelegate,URLSessionDataDelegate
{
public var _response:HTTPURLResponse
public override init()
{
_response = HTTPURLResponse()
}
public func post(data: Data, url: URL)
{
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Keep-Alive", forHTTPHeaderField: "Connection")
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration,delegate: self, delegateQueue: OperationQueue.main)
let task = session.uploadTask(with: request, from: data)
task.resume()
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void)
{
_response = response as! HTTPURLResponse
}
}
however I never even hit the PHPserver. the server when hit will say something like this in the terminal:
[Tue Mar 7 11:43:20 2017] 192.168.250.100:64265 [200]: /
[Tue Mar 7 11:43:20 2017] 192.168.250.100:64266 [404]: /favicon.ico - No such file or directory
Well that is when I hit it with my browser and there is no image with it. but alt least I know that it will write something with the terminal if it hits it. Nothing happens And without a resource to teach me this api I'm afraid I will never learn how to fix this or even if I'm doing something completely wrong.
I'm using Swift 3 and Xcode 8.2.1
Edit:
I've added this method to the class and found that I hit it every single time.
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
{
_error = error.debugDescription
}
the debug description have this string "some"
I never used this exact procedure with tasks but rather use the methods with callback. I am not sure if in the background there should be much of a difference though.
So to generate the session (seems pretty close to your):
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
Then I generate the request which stupidly enough needs an URL in the constructor:
var request = URLRequest(url: URL(string: "www.nil.com")!) // can't initialize without url
request.url = nil
Adding url with query parameters (you can just set the URL in your case, I have a tool to handle a few cases):
fileprivate func injectQueryParameters(request: inout URLRequest) {
if let query = queryParameters.urlEncodedString {
let toReturn = endpoint.url + "?" + query
if let url = URL(string: toReturn) {
request.url = url
} else {
print("Cannot prepare url: \(toReturn)")
}
} else {
let toReturn = endpoint.url
if let url = URL(string: toReturn) {
request.url = url
} else {
print("Cannot prepare url: \(toReturn)")
}
}
}
Then the form parameters. We mostly use JSON but anything goes here:
fileprivate func injectFormParameters( request: inout URLRequest) {
if let data = rawFormData {
request.httpBody = data
} else if let data = formParameters.urlEncodedString?.data(using: .utf8) {
request.httpBody = data
}
}
And the headers:
fileprivate func injectHeaders(request: inout URLRequest) {
headers._parameters.forEach { (key, value) in
if let stringValue = value as? String {
request.setValue(stringValue, forHTTPHeaderField: key)
}
}
}
So in the end the whole call looks something like:
class func performRequest(request: URLRequest, callback: (([String: Any]?, NSError?) -> Void)?) {
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request) { data, response, error in
// Response is sent here
if let data = data {
callback?((try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as [String: Any]?, error)
} else {
callback?(nil, error)
}
}
task.resume()
}
I hope this puts you on the right track. In general you do have a few open source libraries you might be interested in. Alamofire is probably still used in most cases.

NSURLSession URL Response is not cached when larger than a few kilobytes

I am pretty new to programming in IOS with Cocoa and I am using Swift. I fetch data from a JSON API using an NSURLSession data session with custom delegate, not closures. The reason for using custom delegate is that I have to do basic authentication and I also inject a custom cache-control header to control caching behavior (my API doesn’t include any caching related headers in the response at all).
All this works perfectly but only for requests for which the URLSession:dataTask:didReceiveData: method is called only once. As soon as I get larger responses (some 20-30kBytes) that call the didReceivedData method several times, the URLSession:dataTask:willCacheResponse:completionHandler: method doesn’t get called at all, and therefore my response doesn’t get cached. Re-issuing the same request within 5 minutes will issue a request to the server again, which doesn’t happen for requests whose responses only call didReceiveData one single time. The URLSession:task:didCompleteWithError: method is correctly called and proceeded in all cases.
The documentation of the URLSession:dataTask:willCacheResponse:completionHandler: method (https://developer.apple.com/library/IOs/documentation/Foundation/Reference/NSURLSessionDataDelegate_protocol/index.html#//apple_ref/occ/intfm/NSURLSessionDataDelegate/URLSession:dataTask:willCacheResponse:completionHandler:) says this method is only called if the NSURLProtocol handling the request decides to do so, but I don’t really understand what to do to make this happen.
Any feedback and ideas are very welcome!
This is the code that issues the HTTP request:
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
config.URLCache = NSURLCache.sharedURLCache()
//config.URLCache = NSURLCache(memoryCapacity: 512000000, diskCapacity: 1000000000, diskPath: "urlCache")
let urlString = apiUrlForFilter(filter, withMode: mode, withLimit: limit, withOffset: offset)
let url = NSURL(string: urlString)
var policy: NSURLRequestCachePolicy?
if ignoreCache == true {
policy = .ReloadIgnoringLocalCacheData
} else {
policy = .UseProtocolCachePolicy
}
let request = NSURLRequest(URL: url!, cachePolicy: policy!, timeoutInterval: 20)
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.dataTaskWithRequest(request)
task.resume()
I have the following delegate functions implemented:
URLSession:didReceiveChallenge:completionHandler: to handle SSL certificate trust,
URLSession:task:didReceiveChallenge:completionHandler: to handle basic authentication
URLSession:dataTask:didReceiveResponse:completionHandler: to read some specific headers and save them as instance variables
additionally, the important ones with code:
URLSession:dataTask:didReceiveData: to accumulate data as it arrives for larger HTTP request responses:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
if receivedData == nil {
receivedData = NSMutableData()
}
receivedData!.appendData(data)
println("did receive data: \(receivedData!.length) bytes")
}
URLSession:dataTask:willCacheResponse:completionHandler: to inject my own Cache-Control header:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, willCacheResponse proposedResponse: NSCachedURLResponse, completionHandler: (NSCachedURLResponse!) -> Void) {
println("willCacheResponse was called")
let response: NSURLResponse = proposedResponse.response
let httpResponse = response as NSHTTPURLResponse
var headers = httpResponse.allHeaderFields
var modifiedHeaders = headers
modifiedHeaders.updateValue("max-age=300", forKey: "Cache-Control")
let modifiedResponse = NSHTTPURLResponse(URL: httpResponse.URL!, statusCode: httpResponse.statusCode, HTTPVersion: "HTTP/1.1", headerFields: modifiedHeaders)
let cachedResponse = NSCachedURLResponse(response: modifiedResponse!, data: proposedResponse.data, userInfo: proposedResponse.userInfo, storagePolicy: proposedResponse.storagePolicy)
completionHandler(cachedResponse)
}
URLSession:task:didCompleteWithError: to check the complete response for errors and call a callback closure this class gets by initialization to further proceed the result data:
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
session.finishTasksAndInvalidate()
if error != nil {
var string: String?
if errorString != nil {
string = errorString
} else {
string = Helpers.NSURLErrorDomainErrorForCode(error!.code)
}
errorCallback(string!)
return
}
if receivedData == nil {
errorCallback("the query returned an empty result")
return
}
var jsonError: NSError?
let results: AnyObject! = NSJSONSerialization.JSONObjectWithData(receivedData!, options: NSJSONReadingOptions.AllowFragments, error: nil)
if results == nil {
errorCallback("the data returned was not valid JSON")
return
}
let jsonParsed = JSONValue.fromObject(results)
if let parsedAPIError = jsonParsed!["error"]?.string {
errorCallback("API error: \(parsedAPIError)")
return
}
callback(jsonParsed!, self.serverTime!)
}

Resources