Skip to content

Commit 573eafc

Browse files
Detect early workerd exit instead of hanging indefinitely
1 parent bfb6229 commit 573eafc

4 files changed

Lines changed: 71 additions & 3 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
Detect early workerd exit instead of hanging indefinitely
6+
7+
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.
8+
9+
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.

packages/miniflare/src/runtime/index.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,17 @@ class StartupLogBuffer {
209209
`Address already in use (${match[1]}:${match[2]}). Please check that you are not already running a server on this address or specify a different port with --port.`
210210
);
211211
}
212+
213+
// If stderr contains any output, surface it so the user can diagnose
214+
// the failure (e.g. bind errors on IPv6 addresses, permission denied,
215+
// missing libraries, etc.)
216+
const stderr = this.stderrBuffer.join("").trim();
217+
if (stderr.length > 0) {
218+
throw new MiniflareCoreError(
219+
"ERR_RUNTIME_FAILURE",
220+
`The Workers runtime failed to start. There was likely a problem with the workerd binary or your configuration.\nRuntime stderr:\n${stderr}`
221+
);
222+
}
212223
}
213224
}
214225

@@ -246,7 +257,8 @@ export class Runtime {
246257
});
247258
const startupLogBuffer = new StartupLogBuffer();
248259
this.#process = runtimeProcess;
249-
this.#processExitPromise = waitForExit(runtimeProcess);
260+
const processExitPromise = waitForExit(runtimeProcess);
261+
this.#processExitPromise = processExitPromise;
250262

251263
const handleRuntimeStdio =
252264
options.handleRuntimeStdio ??
@@ -279,8 +291,15 @@ export class Runtime {
279291
runtimeProcess.stdin.end();
280292
await once(runtimeProcess.stdin, "finish");
281293

282-
// 4. Wait for sockets to start listening
283-
const ports = await waitForPorts(controlPipe, options);
294+
// 4. Wait for sockets to start listening, racing against the process
295+
// exiting early. If workerd exits before all required sockets report
296+
// their ports (e.g. due to a bind failure), `waitForPorts()` would
297+
// hang indefinitely. Racing against the exit promise ensures we
298+
// detect this and return `undefined` promptly.
299+
const ports = await Promise.race([
300+
waitForPorts(controlPipe, options),
301+
processExitPromise.then(() => undefined),
302+
]);
284303
if (ports?.has(kInspectorSocket) && process.env.VSCODE_INSPECTOR_OPTIONS) {
285304
// We have an inspector socket and we're in a VSCode Debug Terminal.
286305
// Let's startup a watchdog service to register ourselves as a debuggable target
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
// A fake workerd that exits immediately without writing any control messages.
3+
// Used to test that Miniflare detects early workerd exits instead of hanging.
4+
5+
import { arrayBuffer } from "stream/consumers";
6+
7+
// Consume stdin (config passed via stdin) to avoid EPIPE
8+
await arrayBuffer(process.stdin);
9+
10+
// Write an error to stderr to simulate a startup failure
11+
process.stderr.write("error: bind(::1, 0): Address not available\n");
12+
13+
// Exit with non-zero code without writing any listen events to FD3
14+
process.exit(1);

packages/miniflare/test/index.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2746,6 +2746,32 @@ unixSerialTest(
27462746
}
27472747
);
27482748

2749+
// When workerd exits before sending all listen events on FD3, Miniflare should
2750+
// detect this and throw ERR_RUNTIME_FAILURE instead of hanging indefinitely.
2751+
// See https://github.com/cloudflare/workers-sdk/issues/14077
2752+
unixSerialTest(
2753+
"Miniflare: throws ERR_RUNTIME_FAILURE when workerd exits before all sockets are ready",
2754+
async ({ expect, onTestFinished }) => {
2755+
const workerdPath = path.join(FIXTURES_PATH, "crashing-workerd.mjs");
2756+
2757+
const original = process.env.MINIFLARE_WORKERD_PATH;
2758+
process.env.MINIFLARE_WORKERD_PATH = workerdPath;
2759+
onTestFinished(() => {
2760+
if (original === undefined) {
2761+
delete process.env.MINIFLARE_WORKERD_PATH;
2762+
} else {
2763+
process.env.MINIFLARE_WORKERD_PATH = original;
2764+
}
2765+
});
2766+
2767+
const mf = new Miniflare({ script: "" });
2768+
onTestFinished(() => mf.dispose().catch(() => {}));
2769+
2770+
await expect(mf.ready).rejects.toThrow(MiniflareCoreError);
2771+
await expect(mf.ready).rejects.toThrow(/Workers runtime failed to start/);
2772+
}
2773+
);
2774+
27492775
const TIMEZONE_WORKER = `
27502776
export default {
27512777
fetch() {

0 commit comments

Comments
 (0)