Skip to content

Detect early workerd exit instead of hanging indefinitely#14081

Open
dario-piotrowicz wants to merge 1 commit into
mainfrom
dario/14077/worderd-startup
Open

Detect early workerd exit instead of hanging indefinitely#14081
dario-piotrowicz wants to merge 1 commit into
mainfrom
dario/14077/worderd-startup

Conversation

@dario-piotrowicz
Copy link
Copy Markdown
Member

@dario-piotrowicz dario-piotrowicz commented May 28, 2026

When workerd exits during startup before writing all expected listen events to the control file descriptor (e.g. due to an IPv6 bind failure, permission error, or missing library), Miniflare's waitForPorts() would block forever. This caused wrangler dev to stall at "Starting local server..." with no error and no timeout.

The fix races waitForPorts() against the child process exit event so that any unexpected workerd termination is detected immediately. When workerd exits early, Miniflare now throws ERR_RUNTIME_FAILURE with the runtime's stderr output included in the error message, making the root cause diagnosable without external tools.

Important

I've created this PR as an attempt to address #14077, it however does not fix that issue.
But in any case this seems looks like an improvement to me that makes miniflare a bit more robust (and it could potentially save some user some pain?)... although it might be completely unnecessary... I'll leave this to the discretion of the reviewer.... I am totally ok proceeding with this PR or scrapping it entirely


  • 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: bugfix

A picture of a cute animal (not mandatory, but encouraged)

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

🦋 Changeset detected

Latest commit: 573eafc

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

This PR includes changesets to release 5 packages
Name Type
miniflare Patch
@cloudflare/pages-shared Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch
wrangler 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-actions
Copy link
Copy Markdown
Contributor

✅ All changesets look good

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

UnknownError: ProviderInitError

github run

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

@dario-piotrowicz Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

create-cloudflare

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

@cloudflare/deploy-helpers

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

@cloudflare/kv-asset-handler

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

miniflare

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

@cloudflare/pages-shared

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

@cloudflare/unenv-preset

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

@cloudflare/vite-plugin

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

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@14081

@cloudflare/workers-editor-shared

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

@cloudflare/workers-utils

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

wrangler

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

commit: 573eafc

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Now I have a thorough understanding of the changes. Let me analyze:

The core change (in packages/miniflare/src/runtime/index.ts):

  1. Adds a Promise.race between waitForPorts() and the process exit promise, so if workerd exits before writing all listen events, Miniflare detects it promptly instead of hanging.
  2. Adds a new fallback in handleStartupFailure(): if stderr has content but doesn't match the specific "Address already in use" pattern, throw a generic ERR_RUNTIME_FAILURE with the stderr contents.

Let me think about potential issues:

  1. Race condition with stderr buffering: When processExitPromise resolves (process exits), ports will be undefined. Then handleStartupFailure() is called. But there's a question: has all stderr output been buffered by this point? The exit event fires after the process terminates, but Node.js stdio streams can have buffered data that hasn't been consumed yet. However, looking at the code, the stderr is piped through the startupLogBuffer.stderrStream Transform, and handleRuntimeStdio is called to set up consumers. The stderrBuffer accumulates chunks as they pass through the Transform stream. When the process exits, the stdio streams should have already delivered their data since the child process flushes on exit. This is generally safe, though in edge cases with very fast exits, there could be a timing issue. But in practice this should be fine.

  2. The waitForPorts function and control pipe close: When workerd exits, the control pipe (FD3) should also close, which would cause the for await loop to end. So in theory, waitForPorts should also resolve (returning undefined since requiredSockets.length > 0). The Promise.race is belt-and-suspenders, but the comment in the code specifically says waitForPorts() would "hang indefinitely." This is actually true in some scenarios — if the child process is spawned with stdio: ["pipe", "pipe", "pipe", "pipe"] and the FD3 stream is a duplex or there's a Node.js behavior where the readable end doesn't emit close/end synchronously with process exit. The Promise.race is a safe and pragmatic fix.

  3. Test quality: The test correctly uses a fake workerd that exits with stderr output, validates the error type and message. It properly cleans up with onTestFinished. Good.

  4. One subtle issue: Looking at waitForPorts, when processExitPromise wins the race, the waitForPorts function is still running (the for await loop is still active). The readline interface on the control pipe is never explicitly closed by anything in this path. However, this should be fine because dispose() is called in cleanup, which destroys all stdio streams including the control pipe, which will cause the readline interface to end.

  5. No unhandled promise rejection: The losing promise in the Promise.race (the waitForPorts call) could theoretically throw if the controlPipe emits an error after the race is won by processExitPromise. But the controlPipe destruction happens in dispose(), and by that point the updateConfig method has already returned. The waitForPorts promise would be a floating promise in this scenario. However, looking at the code, waitForPorts uses for await on a readline interface, which shouldn't throw on stream destruction — it would just end iteration. So this should be safe.

The changes look correct, well-tested, and solve a real problem. The approach is sound.

LGTM

github run

@dario-piotrowicz dario-piotrowicz marked this pull request as ready for review May 28, 2026 23:05
@workers-devprod workers-devprod requested review from a team and edmundhung and removed request for a team May 28, 2026 23:06
@workers-devprod
Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/fix-miniflare-hang-on-workerd-exit.md: [@cloudflare/wrangler]
  • packages/miniflare/src/runtime/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/fixtures/crashing-workerd.mjs: [@cloudflare/wrangler]
  • packages/miniflare/test/index.spec.ts: [@cloudflare/wrangler]

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

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.

2 participants