fix(eio,eio-client): handle close packets consistently across transports - #5525
GiHoon1123 wants to merge 2 commits into
Conversation
The polling transport already closed the connection upon receiving a
"close" packet, but the WebSocket transport silently ignored it and kept
the connection open, on both the server and the client. This is
inconsistent with the Engine.IO protocol, where "close" is a formal packet
type that any conformant implementation may send over any transport.
Add the same check to the shared base Transport#onData(), on both sides
(this also covers the uWebSockets.js-based server transport, which relies
on the same base method). On the client, match the polling transport's
existing close details ("transport closed by the server") for consistency.
Both fixes call the transport's real close procedure (not just the
internal "closed" state), so the underlying WebSocket connection is
actually torn down instead of being left open once the Engine.IO layer
considers it closed.
Fixes socketio#5286
|
Just checking in on this one. No rush at all, and I'd be happy to make any updates if there's anything else needed from my side. |
darrachequesne
left a comment
There was a problem hiding this comment.
Thanks for the pull request. Please find my review notes below.
| if ("close" === packet.type) { | ||
| debug("got close packet"); | ||
| if (this.readyState === "opening" || this.readyState === "open") { | ||
| this.doClose(); |
There was a problem hiding this comment.
I think onClose() will be called twice here with WebSocket:
this.onClose({ description: "transport closed by the server" });- and then in the "close" handler here:
socket.io/packages/engine.io-client/lib/transports/websocket.ts
Lines 83 to 86 in 3df3fed
Could you please check?
|
Thanks for catching this. I checked the WebSocket path and confirmed that the close packet called onClose() once, then ws.close() triggered the WebSocket onclose handler and called it again. BaseWS.doClose() now clears that handler before closing the socket. The regression test counts Transport.onClose() calls and expects one; it failed with two calls before the change and passes now. The close reason and details checks remain in place. The close-packet tests and format checks pass. The only local failure in the full Engine.IO run is the existing WebTransport test because the native webtransport.node module is not available in this environment. |
Summary
Fixes #5286.
The polling transport already closes the connection when it receives a "close" packet, but the WebSocket transport silently ignored it and kept the connection running - on both the server and the client. This is inconsistent with the Engine.IO protocol, where
closeis a formal packet type (independent of the underlying WebSocket connection's own close frame), so any conformant implementation may send it over any transport.Approach
Added the same check to the shared base
Transport#onData()in bothengine.ioandengine.io-client, rather than duplicating polling's own override:onData().Both fixes call the transport's real close procedure (
close()/doClose()+onClose()), not just the internal state transition - an earlier version of this fix calledonClose()directly (mirroring polling's own shortcut), but that leaves the underlying WebSocket connection open indefinitely, since polling's shortcut is safe only because each poll is a short-lived HTTP request that Node's HTTP server cleans up on its own, which doesn't apply to a persistent WebSocket connection.The WebSocket path also avoids reporting the same close twice. When a close packet calls
doClose(), the underlying WebSocket is closed deliberately; itsonclosehandler is cleared first so it cannot callTransport#onClose()a second time.Out of scope
While investigating, two related gaps were found but intentionally left out of this PR to keep it focused on the reported issue:
WebTransporttransport has the same underlying gap (it decodes and dispatches packets directly, bypassingonData()entirely), but it isn't mentioned in the issue and is disabled by default. Happy to follow up separately if maintainers want it addressed.WebTransportabove.Test plan
onClose()method is called exactly once for a WebSocket close packetgit stashon just that file) and confirmed the corresponding new test fails without it, then restored and reconfirmed it passesreadyStatefield) that the underlying raw WebSocket connection is actually closed on both the server and client side, not just marked closed at the Engine.IO layerengine.iosuite passes for both protocol v4 and legacy v3 (EIO_CLIENT=3) - the client-side test is skipped under legacy v3 mode, since that mode exercises the separately-publishedengine.io-client-v3package rather than theengine.io-clientpackage touched by this PRengine.io-clientsuite passes