URLSessionWebSocketTask and CFNetwork connectivity in Airplane mode

Is there a recommended approach for detecting WebSocket connectivity issues? More specifically, does URLSessionWebSocketTask have a way to access errors within CFNetwork?

My app maintains a WebSocket connection with a remote server, and I would like to notify the user that their connection has dropped when the phone loses network connectivity. I'm simulating a dropped network connection in my test environment by placing my phone in Airplane mode and deactivating WiFi. None of URLSessionWebSocketTask's delegates or callbacks pass back an error, and I only get the following log message:

Connection 1: encountered error(1:53)

I'm using a URLSessionWebSocketDelegate and the urlSession(session:, webSocketTask:, didCloseWith:) does not get triggered, neither does webSocketTask.receive(completionHandler:)

I believe the log messages comes from CFNetwork since I can find the log within the Console app and the log references CFNetwork.

I found that webSocketTask.sendPing(pongReceiveHandler:) does eventually discover that connectivity is lost, but after a really long time. As far as I know, sendPing should probably run in a background task that runs every 30 seconds or so, and the error isn't discovered until about another 30+ seconds when the ping request times out. So, it could take 1+ minute(s) before the user is notified of the issue. I don't really like this approach since it requires extra code, extra network requests, and a very delayed response.

So, do errors within CFNetwork propagate to URLSessionWebSocketTask in any way? If not, I would really like if errors within CFNetwork propagated to webSocketTask.receive(completionHandler:) as a .warning URLSessionWebSocketTask.Message along with an error code. Ultimately, I would like to handle connection errors when they are encountered.

I noticed in another forum post that Apple changed iOS WebSockets in a beta release, so I tried to update my software without any luck. I updating macOS to 10.15.6 Beta (19G60d), iOS to 13.6 (17G5059c), and XCode to Version 12.0 beta 2 (12A6163b). However, XCode couldn't connect to my phone, and webSocketTask.send(message:) had intermittent string encoding issues.

System Details:
macOS 10.15.5 (1 June 2020 update)
iOS 13.5.1 (1 June 2020 update)
XCode Version 11.5 (11E608c)
Answered by ethan.lozano in 628277022
I ended up switched to Apple's swift-nio for websocket functionality. More specifically, I decided to use Vapor's websocket-kit, which is a light wrapper over swift-nio.

Network.framework's websocket implementation just doesn't seem to be ready for production use. I try very hard to use native libraries whenever I can, but URLSessionWebSocketTask was giving me too much trouble. I managed with the quirks of URLSessionWebSocketTask until I encountered an issue that made my application unusable: my server immediately sends information to the client when the client's connection upgrades to a websocket connection, and URLSessionWebSocketTask's input stream would intermittently close. So, either I was using the API incorrectly, or there is some sort of race condition within URLSessionWebSocketTask; either way, I couldn't afford to spend the time to figure out what was wrong. When I switched to swift-nio, all of my websocket woes disappeared.

Here are a few issues that I encountered:
  1. URLSessionWebSocketTask logs ominous error messages even with a .normalClosure. I never figured out if I was using the API incorrectly, or if these error messages were expected.

  2. URLSessionWebSocketTask.receive appears to require recursive calls. This seems to open up the possibilities for race conditions, stack overflows, and overall just unnecessary general confusion. Could the API change to a publisher?

  3. Having to pass a URLSessionWebSocketDelegate into a URLSession means that you can have a strong reference to "self" if your websocket wrapper class implements this protocol. A strong reference means that you open up the possibility for a memory leak since the class isn't disposed when dereferenced. I avoided a strong reference with an inner class object, but I would prefer if the API avoided this issue altogether.

  4. There should be an easier way to discover connection issues. For instance Vapor's websocket-kit simply lets you set a variable (i.e. "ws.pingInterval = TimeAmount.seconds(10)") and the library will automatically close the connection if it doesn't receive a pong within an interval after sending a ping.

  5. As mentioned above, URLSessionWebSocketTask seems to close the input stream if the server sends a message too soon after a connection is upgraded to a websocket connection. What makes things more confusing is that the websocket's output stream still appears to be connected to the server and the server can receive messages, but the server can no longer send messages to the websocket client.

  6. It would be nice if the library differentiated between the initial GET connection, and then when the connection is upgraded to a websocket connection.

Accepted Answer
I ended up switched to Apple's swift-nio for websocket functionality. More specifically, I decided to use Vapor's websocket-kit, which is a light wrapper over swift-nio.

Network.framework's websocket implementation just doesn't seem to be ready for production use. I try very hard to use native libraries whenever I can, but URLSessionWebSocketTask was giving me too much trouble. I managed with the quirks of URLSessionWebSocketTask until I encountered an issue that made my application unusable: my server immediately sends information to the client when the client's connection upgrades to a websocket connection, and URLSessionWebSocketTask's input stream would intermittently close. So, either I was using the API incorrectly, or there is some sort of race condition within URLSessionWebSocketTask; either way, I couldn't afford to spend the time to figure out what was wrong. When I switched to swift-nio, all of my websocket woes disappeared.

Here are a few issues that I encountered:
  1. URLSessionWebSocketTask logs ominous error messages even with a .normalClosure. I never figured out if I was using the API incorrectly, or if these error messages were expected.

  2. URLSessionWebSocketTask.receive appears to require recursive calls. This seems to open up the possibilities for race conditions, stack overflows, and overall just unnecessary general confusion. Could the API change to a publisher?

  3. Having to pass a URLSessionWebSocketDelegate into a URLSession means that you can have a strong reference to "self" if your websocket wrapper class implements this protocol. A strong reference means that you open up the possibility for a memory leak since the class isn't disposed when dereferenced. I avoided a strong reference with an inner class object, but I would prefer if the API avoided this issue altogether.

  4. There should be an easier way to discover connection issues. For instance Vapor's websocket-kit simply lets you set a variable (i.e. "ws.pingInterval = TimeAmount.seconds(10)") and the library will automatically close the connection if it doesn't receive a pong within an interval after sending a ping.

  5. As mentioned above, URLSessionWebSocketTask seems to close the input stream if the server sends a message too soon after a connection is upgraded to a websocket connection. What makes things more confusing is that the websocket's output stream still appears to be connected to the server and the server can receive messages, but the server can no longer send messages to the websocket client.

  6. It would be nice if the library differentiated between the initial GET connection, and then when the connection is upgraded to a websocket connection.

In case anyone on Apple's Network.framework team is listening: below is the WebSocket API that I use in my application to wrap WebSocket implementations. My API doesn't handle every scenario that every application might encounter and it's tailored for my application, but I would really appreciate if Network.framework produced a similar WebSocket API.

Code Block
protocol WebSocketConnection: MessageClient {
  func connect()
  func disconnect()
  var status: AnyPublisher<ConnectionStatus, Never> { get }
  var message: AnyPublisher<String, Never> { get }
}
protocol MessageClient {
  func send(text: String)
}
enum ConnectionStatus {
  case idle
  case connected
  case upgraded(MessageClient)
  case disconnected(DisconnectReason)
  case reconnect(Date)
}
enum DisconnectReason {
  case error(Error)
  case unknown(Int)
  case unauthorized
  case not_found
  case normal
}

Ethan, totally agreed with your analysis and suggested API.

Is anyone from Apple following these threads about the native websocket API? This is not the only one.

Popping in to say Ethan's advice saved me hours of headache. I've fought with the native URLSession and WebSocketTask APIs for about eight months, and used it across our fleet. Out of nowhere, socket connections would make no progress, and just retry every ten seconds. The instructions for debugging CFNetwork were of no use, and I had nearly no logs from CFNetwork explaining what was going on. Setting the debugging environment variable did nothing, the task just wouldn't budge. It's on brand with the overall experience of the Network framework, unfortunately.

I switched over to WebSocketKit and have been beyond pleased. It has a very simple API and makes it very easy to migrate your URLSession code to it, usually with far less code being needed.

There seems to be some serious confusion about Apple’s WebSocket APIs in this thread. There are two APIs for WebSocket [1]:

  • Foundation’s NSURLSession, via NSURLSessionWebSocketTask

  • Network framework, via NWConnection

All of the issues discussed in this thread seem to be related to the Foundation API. That certainly matches my experience. In contrast, I’ve found Network framework’s WebSocket implementation to be solid.

If you’re having problems with the Foundation WebSocket implementation, try using Network framework. If you have problems with that, I recommend that you start a new thread, with the Network tag, and either Matt or I will take a look.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Ignoring the web view and browser stuff.

URLSessionWebSocketTask and CFNetwork connectivity in Airplane mode
 
 
Q