Close listener leak in fs/promises `createReadStream`
还没有人认领这个 Issue。
- 主要语言
- JavaScript
- 星标
- 122k
- 派生
- 37.4k
- 平均合并
- 4 天 3 小时
- 30 天内合并 PR
- 272
描述
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}`);
}
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 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