skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    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.


  2. I was looking for the same solution today. At first, I tried to use session.streamTask, but I didn’t know how to use it. It’s a low-level task for TCP, but what I wanted was an HTTP-level solution. I also didn’t want to use URLConnection, which has been deprecated.

    After some research, I finally figured it out: In the documentation for URLSessionDataDelegate,
    https://developer.apple.com/documentation/foundation/urlsessiondatadelegate

    A URLSession object need not have a delegate. If no delegate is assigned, when you create tasks in that session, you must provide a completion handler block to obtain the data.

    Completion handler blocks are primarily intended as an alternative to using a custom delegate. If you create a task using a method that takes a completion handler block, the delegate methods for response and data delivery are not called.

    The key is to not set a completion handler block in dataTask(), and implement the 2 delegate methods of URLSessionDataDelegate:

    // This will be triggered repeatedly when new data comes
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive: Data) {
        var resultString = String(data: didReceive, encoding: .utf8)
        print("didReceive: (resultString)")
    }
        
    // This will be triggered when the task ends. Handle errors here as well
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print("didCompleteWithError: (error)")
    }
    

    Another key is to set the delegate to the URLSessionDataTask, not URLSession. The problem with Larme’s code is that he set the delegate to URLSession, so the function urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive: Data) will not be called.

    The full code demonstration:

    class NetRequest: NSObject, URLSessionDataDelegate {
    
        func startRequest() {
            var urlRequest = URLRequest(url: "http://...")
            // Set up urlRequest...
            // ...
            
            let session = URLSession(configuration: .default)
            let dataTask = session.dataTask(with: urlRequest)
            dataTask.delegate = self
            dataTask.resume()
        }
    
        func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive: Data) {
            var resultString = String(data: didReceive, encoding: .utf8)
            print("didReceive: (resultString)")
        }
        
        func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
            print("didCompleteWithError: (error)")
        }
    
    }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search