Close listener leak in fs/promises `createReadStream`
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- JavaScript
- Star
- 122k
- Fork
- 37.3k
- Merge trung bình
- 4 ngày 2 giờ
- Pull request đã merge (30 ngày)
- 283
Mô tả
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}`);
}
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu trong lib/internal/fs/streams.js, tại listener được createReadStream thêm vào, sau đó lần theo FileHandle.createReadStream trong lib/internal/fs/promises. Tái hiện vấn đề bằng ví dụ phạm vi lặp lại được cung cấp và thêm hoặc cập nhật phạm vi kiểm thử hồi quy tại nơi hành vi của stream fs/promises được kiểm thử. Hoàn thành khi các stream đã được tiêu thụ không còn tích lũy các listener đóng khi autoClose là false.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- javascript, node.js
- Lĩnh vực
- backend, operating-systems
- Loại issue
- Lỗi
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 74/100