nodejs / nodejs/node

Close listener leak in fs/promises `createReadStream`

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

まだ誰も着手していません。

fs
主要言語
JavaScript
スター
122k
フォーク
37.3k
平均マージ
4日 2時間
マージ済み PR(30日)
283

説明

Version

v24.18.0

Platform
Darwin *.local 24.6.0 Darwin Kernel Version 24.6.0: Tue Apr 21 20:19:12 PDT 2026; root:xnu-11417.140.69.710.16~1/RELEASE_ARM64_T6041 arm64
Subsystem

fs/promises

What steps will reproduce the bug?

Here is a silly example which prints the first 11 bytes of a file by reading them separately from disk:

import { buffer } from 'node:stream/consumers';
import { open } from 'node:fs/promises';

const f = await open('my-file.txt');
for (let i = 0; i < 11; i++) {
  const byte = await buffer(f.createReadStream({ start: i, end: i, autoClose: false }));
  console.log(`byte ${i} is ${byte[0]}. Close listeners = ${f.listeners('close').length}`);
}

Running it (pointing at any file containing at least 11 bytes) will demonstrate the issue (output below)

How often does it reproduce? Is there a required condition?

Every call to createReadStream adds a close listener to the file handle, and I have not found a way to remove this listener. If called at least 11 times, it will trigger Node.js' built-in event leak detection warning. The threshold can be increased to avoid this warning, but the leak remains.

What is the expected behavior? Why is that the expected behavior?

Once a stream is consumed, the close event listener it attaches to the FileHandle should be removed, even when autoClose is false, so that applications can read arbitrarily many ranges from a file.

What do you see instead?
byte 0 is 0. Close listeners = 1
byte 1 is 0. Close listeners = 2
byte 2 is 0. Close listeners = 3
byte 3 is 0. Close listeners = 4
byte 4 is 0. Close listeners = 5
byte 5 is 0. Close listeners = 6
byte 6 is 0. Close listeners = 7
byte 7 is 0. Close listeners = 8
byte 8 is 0. Close listeners = 9
byte 9 is 0. Close listeners = 10
(node:70360) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 close listeners added to [FileHandle]. MaxListeners is 10. Use emitter.setMaxListeners() to increase limit
    at genericNodeError (node:internal/errors:985:15)
    at wrappedFn (node:internal/errors:539:14)
    at _addListener (node:events:590:17)
    at FileHandle.addListener (node:events:608:10)
    at importFd (node:internal/fs/streams:156:16)
    at new ReadStream (node:internal/fs/streams:189:30)
    at FileHandle.createReadStream (node:internal/fs/promises:363:12)
    at file:///[...]/test.mts:6:15
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
byte 10 is 0. Close listeners = 11
Additional information

This can be worked around in user-space with a hacky approach:

function createSafeReadStream(handle, options) {
  const before = handle.listeners('close').length;
  const stream = handle.createReadStream(options);
  const after = handle.listeners('close');
  if (after.length > before) {
    const listener = after[after.length - 1];
    const teardown = () => {
      handle.off('close', listener);
      stream.off('end', teardown);
      stream.off('error', teardown);
    };
    stream.once('end', teardown);
    stream.once('error', teardown);
  }
  return stream;
}

// ...

for (let i = 0; i < 11; i++) {
  const byte = await buffer(createSafeReadStream(f, { start: i, end: i, autoClose: false }));
  console.log(`byte ${i} is ${byte[0]}. Close listeners = ${f.listeners('close').length}`);
}

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

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

調査の方向性

lib/internal/fs/streams.js の createReadStream によって追加される listener から始め、次に lib/internal/fs/promises の FileHandle.createReadStream を追跡します。提供された繰り返し範囲の例で問題を再現し、fs/promises ストリームの動作がテストされている箇所に回帰テストを追加または更新します。autoClose が false の場合に、消費済みのストリームが close listener を蓄積しなくなれば完了です。

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

評価

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

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

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