From e4d317e5b4ff694e4d21ba25998fcf617b5c56c4 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Wed, 6 May 2026 08:08:49 +0300 Subject: [PATCH 1/2] fs: abort in-flight stat operations Signed-off-by: Mert Can Altin --- lib/fs.js | 25 ++++++++++++++-- test/parallel/test-fs-stat-abort-test.js | 37 ++++++++++++++---------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/lib/fs.js b/lib/fs.js index d63fad8b2a258b..2626e2ac45fba9 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -136,6 +136,7 @@ const { const { isInt32, parseFileMode, + validateAbortSignal, validateBoolean, validateBuffer, validateEncoding, @@ -340,6 +341,25 @@ function checkAborted(signal, callback) { return false; } +function bindSignalToReq(req, signal, callback) { + if (!signal) { + req.oncomplete = callback; + return; + } + let aborted = false; + const onAbort = () => { + aborted = true; + callback(new AbortError(undefined, { cause: signal.reason })); + }; + kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; + signal.addEventListener('abort', onAbort, { __proto__: null, [kResistStopPropagation]: true }); + req.oncomplete = function(err, result) { + signal.removeEventListener('abort', onAbort); + if (aborted) return; + callback(err, result); + }; +} + /** * Asynchronously reads the entire contents of a file. * @param {string | Buffer | URL | number} path @@ -1635,11 +1655,12 @@ function stat(path, options = { bigint: false, throwIfNoEntry: true }, callback) callback = makeStatsCallback(callback); path = getValidatedPath(path); + if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal'); if (checkAborted(options.signal, callback)) return; const req = new FSReqCallback(options.bigint); - req.oncomplete = callback; - binding.stat(getValidatedPath(path), options.bigint, req, options.throwIfNoEntry); + bindSignalToReq(req, options.signal, callback); + binding.stat(path, options.bigint, req, options.throwIfNoEntry); } function statfs(path, options = { bigint: false }, callback) { diff --git a/test/parallel/test-fs-stat-abort-test.js b/test/parallel/test-fs-stat-abort-test.js index 2a2b35f8030d7f..1c78f74522e167 100644 --- a/test/parallel/test-fs-stat-abort-test.js +++ b/test/parallel/test-fs-stat-abort-test.js @@ -6,29 +6,34 @@ const assert = require('node:assert'); const fs = require('node:fs'); const tmpdir = require('../common/tmpdir'); -test('fs.stat should throw AbortError when called with an already aborted AbortSignal', async () => { - // This test verifies that fs.stat immediately throws an AbortError if the provided AbortSignal - // has already been canceled. This approach is used because trying to abort an fs.stat call in flight - // is unreliable given that file system operations tend to complete very quickly on many platforms. - tmpdir.refresh(); +tmpdir.refresh(); +const filePath = tmpdir.resolve('temp.txt'); +fs.writeFileSync(filePath, 'Test'); - const filePath = tmpdir.resolve('temp.txt'); - fs.writeFileSync(filePath, 'Test'); - - // Create an already aborted AbortSignal. +test('fs.stat aborts when signal is already aborted', async () => { const signal = AbortSignal.abort(); - const { promise, resolve, reject } = Promise.withResolvers(); fs.stat(filePath, { signal }, (err, stats) => { - if (err) { - return reject(err); - } + if (err) return reject(err); resolve(stats); }); + await assert.rejects(promise, { name: 'AbortError' }); +}); - // Assert that the promise is rejected with an AbortError. +test('fs.stat aborts in-flight when signal aborts after the call', async () => { + const controller = new AbortController(); + const { promise, resolve, reject } = Promise.withResolvers(); + fs.stat(filePath, { signal: controller.signal }, (err, stats) => { + if (err) return reject(err); + resolve(stats); + }); + controller.abort(); await assert.rejects(promise, { name: 'AbortError' }); +}); - fs.unlinkSync(filePath); - tmpdir.refresh(); +test('fs.stat throws ERR_INVALID_ARG_TYPE for invalid signal', () => { + assert.throws( + () => fs.stat(filePath, { signal: 'not-a-signal' }, () => {}), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); }); From face7e95e998c1c5ffef326d10d5243e3679e532 Mon Sep 17 00:00:00 2001 From: Mert Can Altin Date: Wed, 6 May 2026 08:12:03 +0300 Subject: [PATCH 2/2] fs: extend signal option to lstat, fstat and promises API Signed-off-by: Mert Can Altin --- doc/api/fs.md | 24 +++++++++ lib/fs.js | 20 +++++-- lib/internal/fs/promises.js | 63 +++++++++++++++++----- test/parallel/test-fs-stat-abort-test.js | 66 ++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 16 deletions(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index b2231bd20cc420..dcd5e3e6fe7b39 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1478,6 +1478,10 @@ link(2) documentation for more detail.