sdk
sdk copied to clipboard
WebSocket should respond to ping frames even if it has no listener attached
It seems the ping/pong protocol for websockets are not correctly keeping the connection alive in this example:
import 'dart:io';
import 'dart:isolate';
Future<void> main(List<String> arguments) async {
final server = await HttpServer.bind('localhost', 8080);
// start client
final client = await Isolate.spawn((port) async {
final clientSocket = await WebSocket.connect('ws://localhost:$port');
int x = 20;
while (true) {
if (x > 0) {
// send first few times
clientSocket.add('T-$x');
}
x--;
await Future.delayed(Duration(milliseconds: 100));
}
}, server.port);
final request = await server.first;
final serverSocket = await WebSocketTransformer.upgrade(request);
serverSocket.pingInterval = const Duration(seconds: 1);
await for (final msg in serverSocket) {
print(msg);
}
print(
'Done?! Should never happen. '
'Pong messages from client should keep connection open',
);
client.kill();
}
General info
- Dart 3.8.1 (stable) (Wed May 28 00:47:25 2025 -0700) on "macos_x64"
- on macos / Version 15.5 (Build 24F74)
- locale is en-DK
Project info
- sdk constraint: '^3.8.1'
- dependencies:
- dev_dependencies: lints, test
If you comment out the line setting the ping interval then it correctly waits forever.
However, as a user we have no way to tricker ping/pong frames. This should happen automatically behind the scenes. At least if the threads involved are not blocked, which is not the case here.
The output from running the snippet above:
T-20
T-19
T-18
T-17
T-16
T-15
T-14
T-13
T-12
T-11
T-10
T-9
T-8
T-7
T-6
T-5
T-4
T-3
T-2
T-1
Done?! Should never happen. Pong messages from client should keep connection open
@nielsenko There is a code in _WebSocketImpl._fromSocket which responds with a ping if it gets a pong. However this only works if somebody is draining messages... I think this is probably working as intended - naturally if you are not draining messages that indicates some sort of a problem. If you add clientSocket.listen((_) { }); to your example it will start working just fine.
I am going to close this based on the reasoning above - but maybe we should update documentation to make this clear.
@mraleph Thank you for the explanation, and work-around. I can confirm that it works setting up a dummy listener on the client socket.
I think it would make sense to add an implicit listener to respond to ping frames, if the user has not already setup a listener.
Using web-sockets to stream one-way data like this is not an unreasonable use-case, and I think it would make for a less surprising api if I don't have to setup a listener for frames I never actually see.
For others who may swing by. This is not an issue, if you use the new cross-platform web_socket package, even if you use IOWebSocket. This is because IOWebSocket always sets up a listener in the ctor.
I think it would make sense to add an implicit listener to respond to ping frames, if the user has not already setup a listener.
I thought about it a bit more and I think I agree with you.
Though I still think there is some questions about semantics: what happens with other frames? what happens if we pause processing of incoming data frames? If we think that ping/pong mechanism is just for checking the state of the connection - then it makes sense to respond to it always even if nobody consuming data packets. If we think that ping/pong mechanism is for checking that client is actively listening (as in - processing the incoming data) then we should not respond to ping if processing is paused.
@brianquinlan @lrhn do you have an opinion?
I think the web socket semantics should not be affected by us choosing to use a Steam to expose it.
When we do that, we can choose to use either a buffering single-subscription stream, or a broadcast stream that you should listen to in time to not lose events.
I don't know enough about web sockets to say which is more natural for them.
In either case, unless we can pause the other end until someone listens, we should treat the stream as live immediately, and not just when it's listened to.
(If we can pause or delay the other side, then we should probably still respect keep-alive until both ends have connected.)
But as @mraleph says, that depends on the intent of the ping/pong. I'd check the web socket spec for that.
...
Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
response, unless it already received a Close frame. It SHOULD
respond with Pong frame as soon as is practical. Pong frames are
discussed in [Section 5.5.3].
...
So the intent seems to be to confirm connectivity so we should always reply.
Presumably the existing implementation is designed to provide TCP back-pressure until someone starts listening to the WebSocket?
But I don't think that is really possible with the design of WebSockets (see https://issues.chromium.org/issues/41470216).
We can probably just simplify the controller handling:
- subscription.pause();
_controller
- ..onListen = subscription.resume
..onCancel = () {
_subscription!.cancel();
_subscription = null;
}
- ..onPause = subscription.pause
- ..onResume = subscription.resume;
Note to future @brianquinlan reading this:
I think that the intent of _ensureController is to allow us to slow down streams added to the WebSocket - it is orthogonal to the above (the JS WebSocket API has bufferedAmount to track queue bytes).