From 41dc5bacd2f8afa1136c9945d1595aa56bf08729 Mon Sep 17 00:00:00 2001 From: jorge guerrero Date: Sat, 28 Feb 2026 20:47:09 -0500 Subject: [PATCH] fs: avoid unhandled rejection in filehandle readableWebStream close Signed-off-by: jorge guerrero --- lib/internal/fs/promises.js | 6 +- ...t-filehandle-readablestream-error-close.js | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-filehandle-readablestream-error-close.js diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 2f95c4b79e17fd..9cda9597bf4454 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -335,7 +335,11 @@ class FileHandle extends EventEmitter { } = require('internal/webstreams/readablestream'); this[kRef](); this.once('close', () => { - readableStreamCancel(readable); + PromisePrototypeThen( + readableStreamCancel(readable), + undefined, + () => undefined, + ); }); return readable; diff --git a/test/parallel/test-filehandle-readablestream-error-close.js b/test/parallel/test-filehandle-readablestream-error-close.js new file mode 100644 index 00000000000000..b5b854bfbbaa5f --- /dev/null +++ b/test/parallel/test-filehandle-readablestream-error-close.js @@ -0,0 +1,82 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +async function runScript(source) { + const child = await common.spawnPromisified(process.execPath, [ + '--unhandled-rejections=strict', + '-e', + source, + ]); + + assert.strictEqual(child.code, 0, child.stderr); + assert.strictEqual(child.signal, null); + assert.strictEqual(child.stderr, ''); +} + +// Regression: once a readableWebStream read fails, explicitly closing the +// FileHandle must not trigger an unhandled rejection. +(async () => { + await runScript(` + const { closeSync } = require('node:fs'); + const { open } = require('node:fs/promises'); + const assert = require('node:assert'); + + async function consume(readable) { + for await (const _ of readable); + } + + (async () => { + const file = await open(process.execPath); + const readable = file.readableWebStream(); + closeSync(file.fd); + + await assert.rejects(consume(readable), { code: 'EBADF' }); + await assert.rejects(file.close(), { code: 'EBADF' }); + })().catch((err) => { + console.error(err); + process.exitCode = 1; + }); + `); +})().then(common.mustCall()); + +// Edge: BYOB readers should not leak unhandled rejections on the same path. +(async () => { + await runScript(` + const { closeSync } = require('node:fs'); + const { open } = require('node:fs/promises'); + const assert = require('node:assert'); + + (async () => { + const file = await open(process.execPath); + const readable = file.readableWebStream(); + const reader = readable.getReader({ mode: 'byob' }); + closeSync(file.fd); + + await assert.rejects(reader.read(new DataView(new ArrayBuffer(1024))), { + code: 'EBADF', + }); + await assert.rejects(file.close(), { code: 'EBADF' }); + })().catch((err) => { + console.error(err); + process.exitCode = 1; + }); + `); +})().then(common.mustCall()); + +// Safety: successful reads must remain unaffected. +(async () => { + await runScript(` + const { open } = require('node:fs/promises'); + + (async () => { + const file = await open(process.execPath); + for await (const _ of file.readableWebStream()); + await file.close(); + })().catch((err) => { + console.error(err); + process.exitCode = 1; + }); + `); +})().then(common.mustCall());