Connection State Recovery fails if client reconnects before server emits any events ("Zero-Event" reconnect)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 63.2k
- Forks
- 10.3k
- Avg merge
- 11d 20h
- Merged PRs (30d)
- 2
Description
Describe the bug
When connectionStateRecovery is enabled, a client that connects, joins rooms, and disconnects before the server emits any events cannot recover its session on reconnection.
Even if the client reconnects within maxDisconnectionDuration, socket.recovered evaluates to false, a new socket.id is generated, and room memberships / socket.data are lost. This happens because _lastOffset on the client is undefined in the zero event case, causing the server's typeof offset === "string" check in namespace.ts to fail and bypass restoreSession().
To Reproduce
Socket.IO server version: 4.8.1
Server
import { Server } from "socket.io";
const io = new Server(3000, {
connectionStateRecovery: {
maxDisconnectionDuration: 2 * 60 * 1000,
},
});
io.on("connection", async (socket) => {
if (socket.recovered) {
console.log(`RECOVERED: ${socket.id} (rooms: ${Array.from(socket.rooms).join(", ")})`);
return;
}
console.log(`INITIAL CONNECT: ${socket.id}`);
await socket.join("room:alerts");
// Deliberately no events emitted to the client
});
Socket.IO client version: 4.8.1
Client
import { io } from "socket.io-client";
const socket = io("http://localhost:3000", {
reconnection: true,
});
socket.on("connect", () => {
console.log(`connect ${socket.id} (recovered: ${socket.recovered})`);
// Simulate a brief network drop shortly after connecting
setTimeout(() => {
socket.io.engine.close();
}, 200);
});
Expected behavior
When the client reconnects after the transient drop, the server should recognize the pid, restore the previous session (socket.id, room memberships, socket.data), and set socket.recovered = true.
Platform:
- Device: PC / Mac / Server
- OS: Linux / macOS / Windows
- Node.js version:
20.x/22.x
Additional context
-
Root Cause: In
socket.io-client,_lastOffsetremainsundefineduntil a packet of typeEVENTwith an offset is received. On reconnect, the handshake sends{ pid: "<id>", offset: undefined }. Insocket.io/lib/namespace.ts, recovery is gated by:if ( this.server.opts.connectionStateRecovery && typeof sessionId === "string" && typeof offset === "string" ) { session = await this.adapter.restoreSession(sessionId, offset); }Because
offsetisundefined,restoreSession()is skipped entirely despite having a valid activepid. -
Impact: Quiet subscriber channels (e.g. alert rooms, idle game lobbies) lose session state on brief network blips unless application code manually emits a synthetic dummy event on first connect to "prime"
_lastOffset. -
Potential Solutions:
- Include the current server stream offset in the initial
CONNECT(packet type0) handshake payload alongside{ sid, pid }so_lastOffsetis initialized at connection time. - Allow
restoreSession(sessionId, offset)to run whenoffsetisundefined, restoringid,rooms, anddatawithout replaying events.
- Include the current server stream offset in the initial
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the recovery gate in socket.io/lib/namespace.ts and trace how socket.io-client maintains _lastOffset and sends the reconnect handshake. Compare the zero-event path with restoreSession() and existing connection-state recovery behavior. Done means a reconnect without prior events preserves the session id, rooms, data, and reports socket.recovered as true.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js, typescript
- Domain
- backend-api-design, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 56/100