From e3f8993d093ef8f332195618a896ff9a5437bc93 Mon Sep 17 00:00:00 2001 From: jkubo Date: Fri, 11 Sep 2026 20:40:35 +0900 Subject: [PATCH] fix(node): prevent logAndExitProcess from recursing on a broken pipe logAndExitProcess writes the error to the console before shutting down. When stderr is a closed pipe that write raises EPIPE, which surfaces as another uncaught exception and re-enters the handler. calledFatalError is already set by then, so onuncaughtexception routes straight back into logAndExitProcess and the cycle repeats, allocating an Error with a captured stack and a pending client.close() on every pass until V8 aborts with a heap OOM. Guard the function itself rather than the calledFatalError branch, so every caller is covered. Fixes #24337 Co-Authored-By: Claude Opus 5 --- packages/node/src/utils/errorhandling.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/node/src/utils/errorhandling.ts b/packages/node/src/utils/errorhandling.ts index bb22766b5155..3b887b4a494d 100644 --- a/packages/node/src/utils/errorhandling.ts +++ b/packages/node/src/utils/errorhandling.ts @@ -4,10 +4,22 @@ import type { NodeClient } from '../sdk/client'; const DEFAULT_SHUTDOWN_TIMEOUT = 2000; +let isShuttingDown = false; + /** * @hidden */ export function logAndExitProcess(error: unknown): void { + // A second failure while we are already shutting down must not re-enter. + // The console.error below writes to stderr; when that stream is a broken + // pipe the write throws EPIPE, which surfaces as another uncaught + // exception and lands back here, recursing until the heap is exhausted. + if (isShuttingDown) { + global.process.exit(1); + return; + } + isShuttingDown = true; + consoleSandbox(() => { // eslint-disable-next-line no-console console.error(error);