URLSessionWebSocketTask does not notify delegate when connection fails

I observe the following behavior:
  • Have a mac connected to the internet via wifi.

  • Establish a websocket connection with some variation of URLSession.shared.webSocketTask(with ...) .

  • After the socket is established, disconnect your wifi (eg. by disabling from the menu bar).

Result:
Session/Task delegate is informed about a timeout after a long delay (observed delay between 60s-3m).

Expected:
Delegate is informed immediately about the connection error.

Notes:
  • I tried to listen to all available delegate methods for URLSessionWebSocketTask, URLSessionTask, and URLSession.

  • I tried many configuration option that seemed like they could be related (like adjusting timeoutIntervalForRequest of the session or request, the networkServiceType, etc)

  • The underlaying network framework seems to notice the error immediately, as there is a log like this showing up: Connection 1: encountered error(1:53)

  • Sending ping frames every 10 seconds does not change the situation either (simply no pong is returned until the timeout error occurs).

  • There does not seem to be an upside to this delayed error reporting, since even if you reconnect the network immediately, the connection still fails with the timeout error after the delay.

My question:

Might there be a configuration option that I overlooked that gives me the behavior I want?
Or do I misunderstand something fundamental about how this API is supposed to work?

Expected:
Delegate is informed immediately about the connection error.
The underlaying network framework seems to notice the error immediately

Testing this between iOS and macOS I do see the message: 2021-04-19 07:47:36.951398-0700 SUWebSocket[11587:8234941] Connection 1: encountered error(1:53) when Wi-Fi is turned off, but to your point, I am not receiving this in a delegate method. I think the question you may be getting at is how can I detect that my web socket connection is unreachable. Well, I would recommend that you just try to send data on the connection and let the error handle it if your connection is unavailable and then bubble that state up through your logic. If you need to conform logic to the state of the connection for some reason you can also use NWConnection for web socket connections as well. Take a look at an example here.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Thanks for your reply.

Well, I would recommend that you just try to send data on the connection and let the error handle it if your connection is unavailable and then bubble that state up through your logic.

Sending messages in this state completes without any error. Only after the timeout error I mentioned has fired anyways does sending a message result in an error. So this does not really solve the problem.

That is odd behavior as I do see the following when I move the machine offline (no Ethernet or Wi-Fi) and then try to send data:
Code Block
2021-04-20 08:11:20.613161-0700 SUWebSocketMac[5526:76327] Connection 1: received failure notification
2021-04-20 08:11:20.613325-0700 SUWebSocketMac[5526:76327] Connection 1: failed to connect 1:50, reason -1
2021-04-20 08:11:20.613375-0700 SUWebSocketMac[5526:76327] Connection 1: encountered error(1:50)
2021-04-20 08:11:20.615712-0700 SUWebSocketMac[5526:76327] Task <4B976A86-1012-417B-B21A-99CC6779C6EA>.<1> HTTP load failed, 0/0 bytes (error code: -1009 [1:50])
Incoming Receive Error: The Internet connection appears to be offline.
[sendDataMethod] strDate: Tuesday, April 20, 2021 at 8:11:22 AM Pacific Daylight Time
Send Error: The Internet connection appears to be offline.



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Wow, that is peculiar, because I see the following (I tested this on macOS 11.2.3, and also on 10.15).

Code Block
2021-04-21 10:21:37.405632+0200 TestWS[33054:327404] sending message 2
2021-04-21 10:21:37.406341+0200 TestWS[33054:327901] did send message 2, error: <nil>
2021-04-21 10:21:47.404558+0200 TestWS[33054:327404] sending message 3
2021-04-21 10:21:47.405089+0200 TestWS[33054:327901] did send message 3, error: <nil>
2021-04-21 10:21:50.075241+0200 TestWS[33054:328200] Connection 1: encountered error(1:53) ==> Wifi capped here
2021-04-21 10:21:57.403576+0200 TestWS[33054:327404] sending message 4
2021-04-21 10:21:57.404152+0200 TestWS[33054:328571] did send message 4, error: <nil> ==> no error sending a message
2021-04-21 10:22:07.404181+0200 TestWS[33054:327404] sending message 5
2021-04-21 10:22:07.404884+0200 TestWS[33054:328640] did send message 5, error: <nil> ==> no error sending a message
2021-04-21 10:22:17.403530+0200 TestWS[33054:327404] sending message 6
2021-04-21 10:22:17.404037+0200 TestWS[33054:328731] did send message 6, error: <nil> ==> no error sending a message
2021-04-21 10:22:27.403033+0200 TestWS[33054:327404] sending message 7
2021-04-21 10:22:27.403717+0200 TestWS[33054:328816] did send message 7, error: <nil> ==> no error sending a message
2021-04-21 10:22:30.558273+0200 TestWS[33054:328816] [connection] nw_socket_handle_socket_event [C1.1:3] Socket SO_ERROR [60: Operation timed out] ==> finally we get the timeout error
2021-04-21 10:22:30.558689+0200 TestWS[33054:328816] [connection] nw_read_request_report [C1] Receive failed with error "Operation timed out"
2021-04-21 10:22:30.559008+0200 TestWS[33054:328640] [websocket] Read completed with an error Operation timed out
2021-04-21 10:22:37.402583+0200 TestWS[33054:327404] sending message 8
2021-04-21 10:22:37.403010+0200 TestWS[33054:328920] did send message 8, error: Error Domain=kNWErrorDomainPOSIX Code=60 "Operation timed out" UserInfo={NSDescription=Operation timed out} ==> and only after that does sending a message give an error


One can see that after capping the connection, sending messages still completes without any error for 40 seconds, until the timeout error occurs.

This makes me wonder if I misconfigure the session somehow? Basically, what I'm doing is the following:

Code Block Swift
public class WSService: NSObject {
    lazy var queue = OperationQueue()
    lazy var request: URLRequest = {
        var request = URLRequest(url: URL(string: "wss://echo.websocket.org")!)
        request.networkServiceType = .responsiveData
        request.timeoutInterval = 10
        return request
    }()
    lazy var session: URLSession = {
        URLSession(configuration: .ephemeral, delegate: self, delegateQueue: self.queue)
    }()
    lazy var task = session.webSocketTask(with: self.request)
    public func start() {
        self.task.resume()
    }
    var msgCount: Int = 0
    func sendMessage(_ string: String) {
        let theCount = msgCount
        msgCount += 1
        os_log("sending message %i", theCount)
        self.task.send(.string(string)) { error in
            os_log("did send message %i, error: %@", theCount, error == nil ? "<nil>" : error.debugDescription)
            return
        }
    }
}


The only configuration that I am doing differently is setting up the receive method after the webSocketTask is resumed.

Code Block swift
func connectMethod() {
guard let socketURL = URL(string:"...") else {
return
}
let request = URLRequest(url: socketURL)
webSocketTask = webSocketSession?.webSocketTask(with: request)
webSocketTask?.resume()
setupReceive()
}
/* All other methods */
func setupReceive() {
webSocketTask?.receive(completionHandler: { (result) in
switch result {
case .failure(let error):
self.tearDownConnection()
case .success(let message):
switch message {
case .string(let stringData):
/* handle string response here */
case .data(let data):
/* handle data response here */
@unknown default:
print("Unknown default on message")
}
self.setupReceive()
}
})
}
func tearDownConnection() {
webSocketTask?.cancel()
}


If you get stuck, open a TSI and I can take a look at a focused sample.



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
URLSessionWebSocketTask does not notify delegate when connection fails
 
 
Q