Skip to content

[wrangler] Exit quietly when the output pipe is closed early - #15416

Open
jkubo wants to merge 2 commits into
cloudflare:mainfrom
jkubo:fix/wrangler-epipe-oom
Open

[wrangler] Exit quietly when the output pipe is closed early#15416
jkubo wants to merge 2 commits into
cloudflare:mainfrom
jkubo:fix/wrangler-epipe-oom

Conversation

@jkubo

@jkubo jkubo commented Aug 29, 2026

Copy link
Copy Markdown

Fixes #15415.

When a Wrangler command has both stdout and stderr connected to a reader that stops early, Wrangler aborts with a JavaScript heap out-of-memory error instead of exiting:

npx wrangler@4.127.1 whoami 2>&1 | head -c 50

On Linux with systemd-coredump enabled that leaves a ~300 MB core dump per occurrence, containing the account data and OAuth/API token material the process was holding. It mainly affects agent and CI runners, since they capture combined output and close the pipe on timeout; an interactive TTY never yields EPIPE.

Cause

Node ignores SIGPIPE, so the failed write arrives as an asynchronous EPIPE error event. Nothing listens for it on the stdio streams, so it becomes an uncaught exception — and in released builds that path is Sentry's logAndExitProcess, which console.errors the error (writing to the stream that just failed, raising another EPIPE) and only then calls process.exit, deferred behind a transport flush of up to 2s. The second EPIPE re-enters the handler well before the process can die, and each turn allocates another Error with a captured stack, so the heap is exhausted rather than the loop terminating.

The core dump contains ~787,000 copies of the same console.errorSocket._writeGenericafterWriteDispatched stack and 1.57M occurrences of EPIPE.

Fix

Attach an error listener to process.stdout and process.stderr so a broken pipe stops output and exits cleanly, and never becomes an uncaught exception in the first place.

Deliberate scoping:

  • Installed from src/cli.ts only inside the require.main === module branch, so the programmatic API (unstable_dev and friends) does not get process-wide exit handlers installed on its embedder's behalf.
  • Only EPIPE and ERR_STREAM_DESTROYED are treated as a broken pipe. Any other stdio error is re-thrown, preserving today's behaviour rather than silently swallowing it.

Verification

Built from source with SENTRY_DSN set (a dead local address), since a plain pnpm build leaves Sentry uninitialised and does not reproduce:

build wrangler whoami 2>&1 | head -c 50
before this change SIGABRT, heap OOM, core dumped
after this change exits cleanly, 3/3 runs, no core

Unit tests added for the guard, and sentry.test.ts, cli.test.ts and index.test.ts pass. oxlint, oxfmt --check and tsc --noEmit are clean.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this fixes a crash; there is no user-facing API or documented behaviour change.

Note

This is a contribution from an AI agent: Claude Code, Claude Opus 5.


Devin Review

Running a Wrangler command with both stdout and stderr going to a reader that
stops early (`wrangler whoami 2>&1 | head`, or an agent/CI runner that captures
combined output and times out) made Wrangler abort with a JavaScript heap
out-of-memory error instead of exiting.

Node ignores SIGPIPE, so the failed write arrives as an unhandled EPIPE error
event. That became an uncaught exception, and Sentry's handler for those logs
the error with console.error before deferring process.exit behind a transport
flush of up to two seconds. The console.error went to the stream that had just
failed, raising another EPIPE that re-entered the handler well before the
process was allowed to exit. Each turn allocated another Error with a captured
stack, so Wrangler exhausted the heap and aborted.

Attach an error listener to both stdio streams when Wrangler runs as a CLI, so
a broken pipe stops output and exits cleanly instead of ever becoming an
uncaught exception. Other stdio errors are still surfaced, and the programmatic
API is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jkubo
jkubo requested a review from workers-devprod as a code owner August 29, 2026 06:20
@changeset-bot

changeset-bot Bot commented Aug 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 05fa7d2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
wrangler Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Aug 29, 2026
@workers-devprod
workers-devprod requested review from a team and penalosa and removed request for a team August 29, 2026 06:21
@workers-devprod

workers-devprod commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/lucky-pans-repeat.md: [@cloudflare/wrangler]
  • packages/wrangler/bin/cf-wrangler.js: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/utils-handle-broken-pipe.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/cli.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/utils/handle-broken-pipe.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Aug 29, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15416

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15416

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15416

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15416

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15416

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15416

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15416

miniflare

npm i https://pkg.pr.new/miniflare@15416

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15416

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15416

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15416

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15416

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15416

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15416

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15416

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15416

wrangler

npm i https://pkg.pr.new/wrangler@15416

commit: 05fa7d2

`bin/cf-wrangler.js` requires `wrangler-dist/cli.js` and runs delegate verbs
in-process rather than re-spawning, so `require.main === module` is false and
the guard installed for the `wrangler` binary never ran for it.

cf-wrangler never calls `main()`, so it does not initialise Sentry and cannot
hit the heap-exhaustion loop; an unhandled EPIPE there is a noisy but bounded
crash. Guarding it anyway keeps the two binaries consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jkubo

jkubo commented Aug 29, 2026

Copy link
Copy Markdown
Author

Thanks — good catch, and confirmed: bin/cf-wrangler.js requires wrangler-dist/cli.js and runs delegate verbs in-process rather than re-spawning, so require.main === module is false there and the guard never installed.

Fixed in the follow-up commit: handleBrokenPipe is now exported alongside the other cf-wrangler entries in cli.ts and installed at the top of bin/cf-wrangler.js, before any output.

One correction to the severity, for the record: cf-wrangler never calls main(), and setupSentry() is only called from main(). Without Sentry there is no logAndExitProcess, and that is the entire amplifier here — so the delegate could not hit the heap-exhaustion loop this PR is about, only a noisy but bounded uncaught EPIPE. Verified by the A/B in the PR description: a build without SENTRY_DSN takes the same EPIPE and exits cleanly every time. Guarding it anyway, since the two binaries should behave the same.

On test coverage for the delegate specifically: the existing cf-wrangler tests cover args and build as modules, with no precedent for driving bin/cf-wrangler.js as a subprocess, and its unknown-verb path calls process.exit(2) synchronously, which races the asynchronous EPIPE and would make such a test flaky. I verified the wiring manually instead — the export resolves and installs one error listener on each of stdout and stderr — and left the unit tests on handleBrokenPipe itself. Happy to add a subprocess test if a maintainer would prefer one.

@jkubo

jkubo commented Aug 29, 2026

Copy link
Copy Markdown
Author

CI note: Tests (Windows, packages-and-tools) is red on an unrelated flake, not this change.

The single failure is miniflaretest/plugins/r2/s3.spec.ts > unroutable requests match R2's responses, failing with TypeError: fetch failed / Caused by: Error: read ECONNRESET (errno: -4077). This PR only touches packages/wrangler and a changeset — no miniflare files — and the same job was green on the previous commit. Wrangler's own suite had zero failures in that run, including the new utils-handle-broken-pipe.test.ts (5 tests, passing on Windows).

Every other check is green (51 pass). I can't re-run the job from a fork; a maintainer re-run should clear it.

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

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

Wrangler aborts with a heap OOM (and dumps credentials to a core file) when stdout and stderr are both a closed pipe

2 participants