Async URLSessionWebSocketTask.receive does not end when socket is cancelled

I'm having trouble freeing my URLSessionWebSocketTask when I am done using it. This only happens if I try to use the async/await version of the interface, the normal closure-based version works.

My application does to much to share the full source but this class should show the error:

class ExoWebSocket {
    var webSocket: URLSessionWebSocketTask?
    var session: URLSession?

    init(ip: String) {
        var request = URLRequest(url: URL(string: "ws://\(ip)/_EXOSocket/")!)
            
        request.addValue("EXOsocket", forHTTPHeaderField: "Sec-WebSocket-Protocol")
        request.timeoutInterval = 1.0

        session = URLSession(configuration: .default, delegate: nil, delegateQueue: nil)
        webSocket = session?.webSocketTask(with: request)
        webSocket?.resume()

        taskReceive() // leaks one __NSURLSessionWebSocketTask object
        //closureReceive() // works as expected
   }

    deinit {
        webSocket?.cancel(with: .normalClosure, reason: nil)
    }

    private func closureReceive() {
        webSocket?.receive { [weak self] result in
            switch result {
            case .success(let message):
                switch message {
                case .string(let text): print("text: \(text)")
                case .data(let data): print("data count: \(data.count)")
                default: print("Unknown type received from WebSocket")
                }
                self?.closureReceive()
            case .failure(let error):
                print("Error when receiving \(error)")
                self?.webSocket = nil
            }
        }
    }

    private func taskReceive() {
        Task { [weak self] in
            while self?.webSocket != nil {
                let message = try? await self?.webSocket?.receive()
                switch message {
                case .string(let text): print("text: \(text)")
                case .data(let data): print("data count: \(data.count)")
                default: print("Unknown type received from WebSocket")
                }
            }
        }
    }
}

I create an ExoWebSocket object in a ViewController and then exit the ViewController. I see that the deinit function is called, but the webSocket?.cancel(...) does not cause the try? await receive to throw or exit.

Is there anything obvious that I should do differently?

Tested with Xcode 13.4 / iPad OS 15.3 and Xcode 14 beta 6 / iPad OS 16.1 beta.

Interesting. There are a few things you could try here to workaround this. First, you could try setting your Task as an instance variable of your class and setting that to nil when your deinit method is run, therefore ending the receive loop task also. Next, if that does not work you could try using NWConnection with the NWProtocolWebSocket but in this case you will need your own HTTP stack if you need to perform connection upgrade etc..

I tried the task-variable, but it made no difference. Why would the receive loop be stopped from only clearing the variable? I also tried doing task.cancel, but it also does not help.

The NWConnection seems like too much work for the time being. I will use the closure-based approach instead since it does not leak. I've read some other forum posts where the delegate function for close does not get called, so maybe these are simply bugs that may be fixed in the future.

Async URLSessionWebSocketTask.receive does not end when socket is cancelled
 
 
Q