cleanupEmptyChildNamespaces releases a child namespace while a socket is in its middleware, and the cleanup unregisters a newer namespace by name

Đang mở
#5,587 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 2 ngày

@Jeremiah-D đang làm issue này rồi.

Từ ngày 5/11/2026.

  • #5588 của @Jeremiah-D — đang mở

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
54/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
node.js, typescript
Lĩnh vực
backend, networking

Hướng nghiên cứu

Start in dist/parent-namespace.js at the cleanupEmptyChildNamespaces handler in ParentNamespace.createChild; trace how middleware sockets are tracked and how the _nsps entry is removed. Add regression coverage for a socket held in async middleware while another connects and disconnects. Done when broadcasts reach every connected socket and cleanup preserves the currently registered namespace; the payload does not name a test file.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Describe the bug

With cleanupEmptyChildNamespaces: true, a child namespace is released as soon as namespace.sockets.size === 0, even while another socket of that namespace is still in its (async) middleware. That socket then joins the released namespace object, while the next connection gets a new object under the same name. Two consequences:

  1. Broadcasting with io.of(name).emit(...) reaches only the sockets of the newer object; the socket admitted into the released object receives nothing.
  2. When the released object later empties, the cleanup deletes the _nsps entry by name, which unregisters the newer object while its sockets are still connected; io.of(name) then creates yet another object, and broadcasts no longer reach any connected socket.

(Related but different: #4772, cleanup after a socket is rejected in middleware.)

To Reproduce

Socket.IO server version: 4.8.3

const http = require('http');
const { Server } = require('socket.io');
const { io } = require('socket.io-client');

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const once = (emitter, event) => new Promise((resolve) => emitter.once(event, resolve));

const httpServer = http.createServer();
const ioServer = new Server(httpServer, { cleanupEmptyChildNamespaces: true });

let releaseHandshake;
const handshakeGate = new Promise((resolve) => { releaseHandshake = resolve; });
let handshakes = 0;
ioServer.of(/^\/room-\w+$/).use(async (socket, next) => {
  handshakes++;
  if (handshakes === 2) await handshakeGate; // e.g. an auth lookup that takes a while
  next();
});

httpServer.listen(0, async () => {
  const url = `http://localhost:${httpServer.address().port}/room-a`;
  const opts = { forceNew: true, transports: ['websocket'] };

  const first = io(url, opts);
  await once(first, 'connect');
  const nspAtStart = ioServer._nsps.get('/room-a');

  const held = io(url, opts); // its handshake waits in the middleware
  await sleep(100);
  first.disconnect(); // the only connected socket leaves: the namespace is released
  await sleep(100);
  console.log('1. namespace released while "held" is still in its middleware:',
    ioServer._nsps.get('/room-a') !== nspAtStart);

  releaseHandshake();
  await once(held, 'connect'); // "held" joins the released namespace object
  const third = io(url, opts);
  await once(third, 'connect'); // a new namespace object is created under the same name

  const received = { held: 0, third: 0 };
  held.on('news', () => received.held++);
  third.on('news', () => received.third++);
  ioServer.of('/room-a').emit('news');
  await sleep(200);
  console.log('2. broadcast to of("/room-a") with two connected sockets:', received,
    '(expected { held: 1, third: 1 })');

  held.disconnect(); // the released object empties: cleanup deletes the _nsps entry by name
  await sleep(200);
  console.log('3. after "held" leaves, the namespace "third" is in is still registered:',
    ioServer._nsps.get('/room-a') != null);
  received.third = 0;
  ioServer.of('/room-a').emit('news');
  await sleep(200);
  console.log('4. broadcast to of("/room-a") with "third" still connected:', { third: received.third },
    '(expected { third: 1 })');

  third.disconnect();
  ioServer.close();
  process.exit(0);
});

Output (socket.io 4.8.3, socket.io-client 4.8.3, Node.js 24):

1. namespace released while "held" is still in its middleware: true
2. broadcast to of("/room-a") with two connected sockets: { held: 0, third: 1 } (expected { held: 1, third: 1 })
3. after "held" leaves, the namespace "third" is in is still registered: false
4. broadcast to of("/room-a") with "third" still connected: { third: 0 } (expected { third: 1 })

Expected behavior

  • A namespace with a socket still in its middleware (_preConnectSockets) is not released, or a socket finishing its handshake joins the namespace object currently registered under that name.
  • The cleanup removes the _nsps entry only if it still points to the object being released (if (this._nsps.get(name) === namespace) this._nsps.delete(name)).

Platform:

  • Server: Node.js 24, macOS and Linux

Additional context

The release check is in ParentNamespace.createChild (dist/parent-namespace.js, the cleanupEmptyChildNamespaces handler on namespace.sockets.size === 0), which does not consider namespace._preConnectSockets. We work around it on our side by keeping per-namespace-object state instead of per-name state.

Ngôn ngữ chính
TypeScript
Star
63.2k
Fork
10.2k
Merge trung bình
1 ngày 23 giờ
Pull request đã merge (30 ngày)
4

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của socketio/socket.io

Tất cả issue của socketio/socket.io

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.