From b5610d3cc286ab7e268be8f4f26e989012d5a4f9 Mon Sep 17 00:00:00 2001 From: GiHoon1123 Date: Mon, 13 Jul 2026 15:53:54 +0900 Subject: [PATCH] fix(eio,eio-client): handle close packets consistently across transports 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 #5286 --- packages/engine.io-client/lib/transport.ts | 8 ++++ packages/engine.io/lib/transport.ts | 8 +++- packages/engine.io/test/server.js | 50 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/engine.io-client/lib/transport.ts b/packages/engine.io-client/lib/transport.ts index ab72c70961..80d2c7da3d 100644 --- a/packages/engine.io-client/lib/transport.ts +++ b/packages/engine.io-client/lib/transport.ts @@ -139,6 +139,14 @@ export abstract class Transport extends Emitter< */ protected onData(data: RawData) { const packet = decodePacket(data, this.socket.binaryType); + if ("close" === packet.type) { + debug("got close packet"); + if (this.readyState === "opening" || this.readyState === "open") { + this.doClose(); + this.onClose({ description: "transport closed by the server" }); + } + return; + } this.onPacket(packet); } diff --git a/packages/engine.io/lib/transport.ts b/packages/engine.io/lib/transport.ts index 6d3b9cbf62..968f1558d3 100644 --- a/packages/engine.io/lib/transport.ts +++ b/packages/engine.io/lib/transport.ts @@ -162,7 +162,13 @@ export abstract class Transport extends EventEmitter { * @protected */ protected onData(data: RawData) { - this.onPacket(this.parser.decodePacket(data)); + const packet = this.parser.decodePacket(data); + if ("close" === packet.type) { + debug("got close packet"); + this.close(); + return; + } + this.onPacket(packet); } /** diff --git a/packages/engine.io/test/server.js b/packages/engine.io/test/server.js index bbfa860681..253af153f1 100644 --- a/packages/engine.io/test/server.js +++ b/packages/engine.io/test/server.js @@ -1604,6 +1604,56 @@ describe("server", () => { }); }); + it("should close transport upon receiving a close packet (ws)", (done) => { + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + engine.on("connection", (conn) => { + conn.transport.on("close", done); + }); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + socket.on("open", () => { + // bypass the client library entirely and send a raw close packet + // ("1", per the Engine.IO protocol), to make sure the server + // reacts the same way regardless of which client implementation + // is on the other end + socket.transport.ws.send("1"); + }); + }); + }); + + if (!IS_CLIENT_V3) { + // the "engine.io-client-v3" package used when IS_CLIENT_V3 is set is a + // separately published legacy dependency, not the "engine.io-client" + // package touched by this fix, so this test only applies to the + // current (v4) client + it("should make the client close its transport upon receiving a close packet (ws)", (done) => { + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + engine.on("connection", (conn) => { + // simulate a server sending a close packet without necessarily + // closing the underlying connection itself right away (e.g. a + // different server implementation), to make sure the client + // reacts to the packet itself rather than relying on the + // underlying WebSocket connection being closed by the server + conn.transport.send([{ type: "close" }]); + }); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + socket.on("close", (reason, description) => { + expect(reason).to.eql("transport close"); + // same wording as the polling transport, for consistency + expect(description.description).to.eql( + "transport closed by the server", + ); + done(); + }); + }); + }); + } + it("should close transport upon parse error (polling)", (done) => { const opts = { allowUpgrades: false, transports: ["polling"] }; const engine = listen(opts, (port) => {