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

オープン
#5,587 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 2 日以内に返信

@Jeremiah-D がすでに取り組んでいます。

2026年11月5日 から。

  • #5588 @Jeremiah-D による — オープン

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
54/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
node.js, typescript

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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.

主要言語
TypeScript
スター
63.2k
フォーク
10.2k
平均マージ
1日 23時間
マージ済み PR(30日)
4

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

socketio/socket.io のほかの issue

socketio/socket.io の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。