From c5c95fac2b9ec90e3e8eb31512d5d738fbad8768 Mon Sep 17 00:00:00 2001 From: Mujeeb Date: Sat, 1 Aug 2026 11:51:37 +0100 Subject: [PATCH] Fix event dispatch before disconnection --- packages/socket.io/lib/socket.ts | 38 +++++++- packages/socket.io/test/socket.ts | 141 ++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 4 deletions(-) diff --git a/packages/socket.io/lib/socket.ts b/packages/socket.io/lib/socket.ts index 96d11a5573..02a81d6d79 100644 --- a/packages/socket.io/lib/socket.ts +++ b/packages/socket.io/lib/socket.ts @@ -36,6 +36,23 @@ import { const debug = debugModule("socket.io:socket"); +function hasBinary(data: unknown): boolean { + if (!data || typeof data !== "object") { + return false; + } + if ( + Buffer.isBuffer(data) || + data instanceof ArrayBuffer || + ArrayBuffer.isView(data) + ) { + return true; + } + if (Array.isArray(data)) { + return data.some(hasBinary); + } + return Object.values(data).some(hasBinary); +} + const RECOVERABLE_DISCONNECT_REASONS: ReadonlySet = new Set([ "transport error", "transport close", @@ -571,7 +588,10 @@ export class Socket< listener.apply(this, args); } } - this.dispatch(args); + this.dispatch( + args, + packet.type === PacketType.BINARY_EVENT || hasBinary(args), + ); } /** @@ -817,10 +837,12 @@ export class Socket< * @param {Array} event - event that will get emitted * @private */ - private dispatch(event: Event): void { + private dispatch(event: Event, isBinary: boolean): void { debug("dispatching an event %j", event); + let isSynchronous = true; + this.run(event, (err) => { - process.nextTick(() => { + const invoke = () => { if (err) { return this._onerror(err); } @@ -829,8 +851,16 @@ export class Socket< } else { debug("ignore packet received after disconnection"); } - }); + }; + + if (isSynchronous && !isBinary && !err) { + invoke(); + } else { + process.nextTick(invoke); + } }); + + isSynchronous = false; } /** diff --git a/packages/socket.io/test/socket.ts b/packages/socket.io/test/socket.ts index 41882a7987..9a93bac40f 100644 --- a/packages/socket.io/test/socket.ts +++ b/packages/socket.io/test/socket.ts @@ -9,6 +9,7 @@ import { } from "./support/util"; import { Server } from ".."; import expect from "expect.js"; +import { PacketType } from "socket.io-parser"; describe("socket", () => { it("should not fire events more than once after manually reconnecting", (done) => { @@ -907,6 +908,146 @@ describe("socket", () => { }); }); + it("should dispatch a non-binary event before disconnect cleanup", (done) => { + const io = new Server(0); + const clientSocket = createClient(io); + + io.on("connection", (socket) => { + let anyCalled = false; + let namedCalled = false; + + socket.onAny((event) => { + if (event === "before-disconnect") { + anyCalled = true; + } + }); + socket.on("before-disconnect", () => { + namedCalled = true; + }); + + (socket as any).onevent({ + type: PacketType.EVENT, + data: ["before-disconnect"], + }); + (socket as any).ondisconnect(); + + expect(anyCalled).to.be(true); + expect(namedCalled).to.be(true); + success(done, io, clientSocket); + }); + }); + + it("should dispatch a non-binary event after synchronous middleware", (done) => { + const io = new Server(0); + const clientSocket = createClient(io); + + io.on("connection", (socket) => { + let namedCalled = false; + + socket.use((_event, next) => { + next(); + }); + socket.on("before-disconnect", () => { + namedCalled = true; + }); + + (socket as any).onevent({ + type: PacketType.EVENT, + data: ["before-disconnect"], + }); + (socket as any).ondisconnect(); + + expect(namedCalled).to.be(true); + success(done, io, clientSocket); + }); + }); + + it("should ignore an event when middleware completes after disconnect", (done) => { + const io = new Server(0); + const clientSocket = createClient(io); + + io.on("connection", (socket) => { + let namedCalled = false; + + socket.use((_event, next) => { + setImmediate(next); + }); + socket.on("before-disconnect", () => { + namedCalled = true; + }); + + (socket as any).onevent({ + type: PacketType.EVENT, + data: ["before-disconnect"], + }); + (socket as any).ondisconnect(); + + setImmediate(() => { + expect(namedCalled).to.be(false); + success(done, io, clientSocket); + }); + }); + }); + + it("should ignore an event received after disconnection", (done) => { + const io = new Server(0); + const clientSocket = createClient(io); + + io.on("connection", (socket) => { + let namedCalled = false; + + socket.on("before-disconnect", () => { + namedCalled = true; + }); + + (socket as any).ondisconnect(); + (socket as any).onevent({ + type: PacketType.EVENT, + data: ["before-disconnect"], + }); + + setImmediate(() => { + expect(namedCalled).to.be(false); + success(done, io, clientSocket); + }); + }); + }); + + it("should dispatch non-binary events before disconnect cleanup for many clients", (done) => { + const io = new Server(0); + const clients = []; + let anyCalled = 0; + let namedCalled = 0; + let disconnected = 0; + + io.on("connection", (socket) => { + socket.onAny((event) => { + if (event === "before-disconnect") { + anyCalled++; + } + }); + socket.on("before-disconnect", () => { + namedCalled++; + }); + socket.on("disconnect", () => { + if (++disconnected === 40) { + expect(anyCalled).to.be(40); + expect(namedCalled).to.be(40); + success(done, io, ...clients); + } + }); + }); + + for (let i = 0; i < 40; i++) { + const clientSocket = createClient(io); + clients.push(clientSocket); + clientSocket.on("connect", () => { + clientSocket.emit("before-disconnect"); + clientSocket.disconnect(); + }); + } + }); + it("should leave all rooms joined after a middleware failure", (done) => { const io = new Server(0); const client = createClient(io, "/");