From 3d328193d69a63a355644c51bfb70f524d99bca9 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:22:09 +0000 Subject: [PATCH] fix(process): apply one timeout rule to both execFileOutput modes Node's execFile timeout contract differs by supported runtime: Node 22 and 24 accept any non-negative integer (their timers clamp anything above 2^31-1 to 1 ms), while Node 26 caps it at Number.MAX_SAFE_INTEGER. Mirroring either left stoppable and plain commands disagreeing on one runtime. Validate a safe non-negative integer before spawning in both modes, so every supported runtime gives the same answer. Co-Authored-By: Claude Opus 5.5 --- src/process/ExecFileOutput.test.ts | 4 ++-- src/process/ExecFileOutput.ts | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/process/ExecFileOutput.test.ts b/src/process/ExecFileOutput.test.ts index 6a5eb5b2e3..f4d690e336 100644 --- a/src/process/ExecFileOutput.test.ts +++ b/src/process/ExecFileOutput.test.ts @@ -123,8 +123,8 @@ describe.skipIf(process.platform === "win32")( expect(await readFile(stopped, "utf8")).toBe("SIGINT"); }); - it.each([-1, Number.NaN, 1.5, Number.POSITIVE_INFINITY])( - "refuses a malformed timeout (%s) before starting the command, as execFile does", + it.each([-1, Number.NaN, 1.5, Number.POSITIVE_INFINITY, 2 ** 53])( + "refuses a malformed timeout (%s) in both modes before starting the command", async (timeout) => { // Starting this command would fail with ENOENT instead. await expect( diff --git a/src/process/ExecFileOutput.ts b/src/process/ExecFileOutput.ts index 94a73aa9f6..dd6aaae620 100644 --- a/src/process/ExecFileOutput.ts +++ b/src/process/ExecFileOutput.ts @@ -54,8 +54,10 @@ const timeoutError = ( }; /** - * Node's execFile contract for `timeout`. A stoppable command keeps its own - * deadline, so it must refuse the values execFile would refuse. + * The `timeout` contract for both modes, checked before spawning. A stoppable + * command keeps its own deadline, so execFile never validates it. Node 26's + * execFile also caps it at Number.MAX_SAFE_INTEGER; Node 22 and 24 accept + * larger integers, which their timers then clamp to 1 ms. */ const invalidTimeout = (timeout: number): RangeError | undefined => { if (Number.isSafeInteger(timeout) && timeout >= 0) return undefined; @@ -106,10 +108,7 @@ export const execFileOutput = ( ): Promise<{ readonly stdout: string; readonly stderr: string }> => new Promise((resolve, reject) => { const { stopSignal, signal, timeout, ...execOptions } = options; - const invalid = - stopSignal === undefined || timeout === undefined - ? undefined - : invalidTimeout(timeout); + const invalid = timeout === undefined ? undefined : invalidTimeout(timeout); if (invalid !== undefined) { reject(invalid); return;