URLProtocol timeout for post/put/patch requests

Hi,


when using a custom URLProtocol in combination with a POST/PUT/PATCH request and an endpoint returning a 401 auth header, the URLSession just times out. It is working as expected for a GET request.


Any idea why it is failing for requests sending data to the server?


I've created a playground to demonstrate the issue:


import Foundation
import PlaygroundSupport
class MyURLProtocol: URLProtocol, URLSessionDelegate {
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    override func startLoading() {
        let session = URLSession(configuration: URLSessionConfiguration.default)
        let sessionTask = session.dataTask(with: self.request, completionHandler: { (data, response, error) in
            if let error = error {
                debugPrint("proto error: ", error.localizedDescription)
                self.client?.urlProtocol(self, didFailWithError: error)
                self.client?.urlProtocolDidFinishLoading(self)
                return
            }
            if let response = response, let httpResponse = response as? HTTPURLResponse {
                let statusCode = httpResponse.statusCode
                debugPrint("proto code: ", statusCode, httpResponse.allHeaderFields)
                self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
                if let data = data {
                    self.client?.urlProtocol(self, didLoad: data)
                }
                self.client?.urlProtocolDidFinishLoading(self)
                return
            }
            self.client?.urlProtocol(self, didFailWithError: NSError(domain: "unknown", code: 0, userInfo: nil))
            self.client?.urlProtocolDidFinishLoading(self)
        })
        sessionTask.resume()
    }
}
class Test: NSObject, URLSessionDelegate {
    func run() {
        let config = URLSessionConfiguration.default
        var protos = config.protocolClasses ?? []
        protos.insert(MyURLProtocol.self, at: 0)
        config.protocolClasses = protos
        config.timeoutIntervalForRequest = 5
        config.timeoutIntervalForResource = 5
        let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
        let url = URL(string: "h t t p : / / dan . shaula . uberspace . de / basic")
        var request = URLRequest(url: url!)
        request.httpMethod = "PATCH"
        request.httpBody = "{\"test\":[{\"key\":\"val\"}]}".data(using: .utf8)
        let task = session.dataTask(with: request) { (data, response, error) in
            if let error = error {
                debugPrint("org req error: ", error.localizedDescription)
            }
            if let response = response as? HTTPURLResponse {
                debugPrint("org req code: ", response.statusCode)
            }
        }
        task.resume()
    }
}
Test().run()
PlaygroundPage.current.needsIndefiniteExecution = true


(Please remove the spaces in the url string before running it.)

Implementing a recursive

URLProtocol
subclass is way more complex than this. At a minimum:
  • You should be taking steps to prevent infinite recursion

  • You must call the

    URLProtocolClient
    in the correct context
  • These’s a specific run loop mode technique you need to use to avoid an incompatibility with web views

You should take a look at the CustomHTTPProtocol sample code to learn more about this. In addition to the code, don’t forget to read the read me, which has a bunch of important info about how to approach this.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi Quinn,


thanks for your reply. You are absolutely right. However this is just a very stripped-down playground snippet to show the issue, that the completion handler of the data task in startLoading is never called. This does not happen for get requests. The things you've pointed out are not related to this behaviour, are they?

The things you've pointed out are not related to this behaviour, are they?

Agreed. I enabled CFNetwork diagnostic logging on your test and it shows that your request goes out on the wire, gets the 401 response, and then triggers an infinite sequence of

Protocol Enqueue
messages. I’m not sure what’s causing that.

At this point I recommend that you try the same test with CustomHTTPProtocol and see what it does in that situation.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks again.


It seems to be related to request.httpBody. It is nil however request.httpBodyStream is set. When I reassign httpBody with the stream data before sending the request in startLoading it is working.


override func startLoading() {
   var request = self.rrequest
    if let httpBodyStream = request.httpBodyStream, request.httpBody == nil {
        request.httpBody = httpBodyStream.toData()
    }
    [..]
}

extension InputStream {
    func toData() -> Data {
        var result = Data()
        var buffer = [UInt8](repeating: 0, count: 4096)
        open()
        var amount = 0
        repeat {
            amount = read(&buffer, maxLength: buffer.count)
            if amount > 0 {
                result.append(buffer, count: amount)
            }
        } while amount > 0
        close()
        return result
    }
}


Am I doing something wrong here or is this a bug in URLProtocol?

Request body streams are one of the many complexities of

NSURLSession
that are not properly supported by
NSURLProtocol
. The problem is that streams can only be read once, and there are situations where the protocol needs to be able to transmit the body multiple times. For example, if you make a request to a server and the server returns a redirect, you have to send that same request to the new server, and that involves replaying the request body stream.
NSURLSession
handles this via the
-URLSession:task:needNewBodyStream:
delegate callback. However, there is no equivalent callback in the
NSURLProtocolClient
protocol. Thus, a protocol implementation can’t fully support body streams.

The standard workaround here is for the protocol to spool the body stream to disk so that it can replay it if necessary. This is a workaround, not a full solution, because it doesn’t support interactive body streams.

When you spool the body stream to disk you really want to do that work asynchronously, with the stream scheduled on the run loop associated with your

NSURLProtocol
subclass. Doing the read synchronously, as you’re doing right now, will deadlock if the body stream relies on CFNetwork infrastructure (for example, if the body stream is a
CFSocketStream
).

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
URLProtocol timeout for post/put/patch requests
 
 
Q