Sitelet https://github.com/Serverless-Devs/Serverless-Devs/pull/960
Skip to content

fix: exit on EPIPE instead of infinite uncaughtException loop and unbounded spawn - #960

Open
weisanju wants to merge 1 commit into
Serverless-Devs:masterfrom
weisanju:fix/epipe-infinite-loop
Open

weisanju wants to merge 1 commit into
Serverless-Devs:masterfrom
weisanju:fix/epipe-infinite-loop

Conversation

@weisanju

Copy link
Copy Markdown

Fix bugs

Bug detail

When a command error occurs while stdout is connected to a pipe whose reader exits early (e.g. s info -t s.yaml 2>&1 | head -5), the CLI enters a never-ending error-handling loop and piles up node processes:

  1. Any error (e.g. ${env('X')} referencing an unset environment variable in s.yaml) is reported by handleError, which writes the error message to stdout and spawns a report.js telemetry daemon (src/exec-daemon.ts).
  2. Once the pipe reader (e.g. head) has exited, every further write to stdout fails with EPIPE, which surfaces as an uncaughtException.
  3. uncaughtException → handleError → writes to stdout again + spawns another daemon → EPIPE → …

Each cycle spawns one detached node child (lib/daemon/report.js, alive ~30s), so the process table fills up until the system pid limit. The CLI process itself never exits on its own; repeated EPIPE records also grow the log file without bound (~16 cycles/s on Linux, much faster on macOS).

Reproduced in a resource-limited container (node:22, --pids-limit, no network) with timeout 15s as watchdog:

scenario result
@serverless-devs/s@3.1.10 (npm), error + | head -5 killed by watchdog, 246 EPIPE cycles logged, 78 concurrent node processes
master (base of this PR), error + | head -5 killed by watchdog, 68 EPIPE cycles logged, 36 concurrent node processes
this fix, same command exits on its own in ~200 ms, 1 process, error still recorded in ~/.s/logs

Fix (standard Unix SIGPIPE semantics — when the pipe reader is gone, exit instead of writing again):

  • register error listeners on process.stdout / process.stderr that exit on EPIPE (src/index.ts);
  • short-circuit EPIPE in the uncaughtException handler instead of re-entering the error-handling flow.

Pull request tasks

  • Add test cases for the changes
  • Passed the CI test

Regression test in __tests__/error.test.ts: pipes the CLI output to an early-exiting reader. Before the fix the spawnSync call is killed by its timeout (the pipeline never exits, ETIMEDOUT); with the fix it finishes in ~200 ms.

When stdout is a pipe whose reader exits early (e.g. `s ... | head`),
writes after that fail with EPIPE. The uncaughtException handler passes
the error to handleError, which writes to stdout again and spawns a
report.js telemetry daemon per cycle, forming a self-sustaining loop:
the process never exits and node child processes pile up.

- register EPIPE guards on process.stdout/stderr (Unix SIGPIPE semantics)
- short-circuit EPIPE in the uncaughtException handler
- add a regression test that pipes `s` output to an early-exiting reader

Signed-off-by: 肖佳权 <1259103745@qq.com>
@weisanju
weisanju force-pushed the fix/epipe-infinite-loop branch from 8d0810d to 8893866 Compare September 16, 2026 01:10

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant