cloudflare / cloudflare/workers-sdk

Regression of #4562: `Error inside ProxyWorker` kills `wrangler dev`, and the printed error is empty

Open
#15,317 12 comments 5 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
4.5k
Forks
1.5k
Avg merge
3d 8h
Merged PRs (30d)
187

Description

This is the same symptom as #4562 ("Network connection lost." surfaced as "Error inside ProxyWorker"), which was closed in the 3.x line. We see it on 4.125.0 through a different code path, so I am filing it as a regression rather than a new report.

### What versions & operating system are you using?

The failure happens **only in CI**, so the workstation `envinfo` would describe a machine where the bug does not occur. The environment that matters:

- **GitHub Actions `ubuntu-latest`, private repository** — 2 cores, 8 GB
- Node 24
- `wrangler` 4.125.0
- `@cloudflare/workerd-linux-64` 1.20260820.1
- `miniflare` 5.20260820.0-alpha
- Next.js 16.2.12 via `@opennextjs/cloudflare`
- **Two `wrangler dev` servers running concurrently** on the same runner

### Please provide a link to a minimal reproduction

**I do not have one, and I would rather say so than link something that does not reproduce.**

The trigger is environmental — a connection dropped mid-response, which we only ever see under contention on a small runner. But the defect itself is in `wrangler`'s own source and does not depend on our code: the path below produces an empty `Error` unconditionally, for any project. The analysis is source-level, and I have included the exact code so it can be checked without running anything.

Happy to provide more logs or to test a patch.

### Describe the Bug

`wrangler dev` exits mid-session with:

```
✘ [ERROR]
🪵 Logs were written to ~/.config/.wrangler/logs/wrangler-.log
ERROR Wrangler dev command failed:
```

The `[ERROR]` line carries **no message at all**. The real one is in the log file, at debug level:

```
Error in ProxyController: Error inside ProxyWorker
at castErrorCause (wrangler-dist/cli.js)
at ProxyController.emitErrorEvent
at ProxyController.onProxyWorkerMessage
cause: {
name: 'Error',
message: 'Network connection lost.',
stack: 'Error: Network connection lost.'
}
```

So the information exists. It is simply not on the line a user reads.

**Why the printed error is empty.** `ProxyWorker.js` posts the failure to the controller as a plain object, because an `Error` cannot cross the worker boundary:

```js
void fetch(userWorkerUrl, new Request(request, { headers }))
.then(...)
.catch((error) => {
if (isSameUserWorkerOrigin(userWorkerUrl, this.proxyData?.userWorkerUrl)) {
void sendMessageToProxyController(this.env, {
type: "error",
error: { name: error.name, message: error.message,
stack: error.stack, cause: error.cause },
});
```

`cli.js` then reconstructs it:

```js
function castErrorCause(cause) {
if (cause instanceof Error) return cause;
const error = new Error(); // no message
error.cause = cause;
return error;
}
```

The posted value is never an `Error`, so this path always yields an `Error` with an empty message. `emitErrorEvent` dispatches it, and the dev server stops.

**wrangler already treats two neighbouring conditions as non-fatal.** `DevEnv.handleErrorEvent` has an exemption branch, and it is two reasons wide:

```js
handleErrorEvent(event) {
if (event.cause instanceof MiniflareCoreError && event.cause.isUserError())
this.emit("error", new UserError(...));
else if (event.source === "ProxyController" &&
(event.reason.startsWith("Failed to send message to") ||
event.reason.startsWith("Could not connect to InspectorProxyWorker"))) {
logger.debug(`Error in ${event.source}: ${event.reason}`, event.cause);
logger.debug("=> Error contextual data:", event.data);
} else this.emit("error", event); // fatal
}
```

The design already recognises that a ProxyController error can be transient and should only be logged. `Error inside ProxyWorker` — a rejected proxy fetch — is the same kind of condition, and falls through to the fatal `else`. There is no configuration escape: the fatality is hardcoded.

**Why it matters beyond a missing message.** The rejection is transient: one connection cut mid-response is enough. A single one takes the whole dev server down, and every request after it fails. In our end to end suite that is 169 tests failing for one dropped connection, with nothing on the console to say why.

**Two suggestions, in order of value:**

1. `castErrorCause` could rebuild a real `Error` from the posted shape, which already carries `name`, `message` and `stack`. The information exists and is discarded at the exact moment a user needs it.
2. `Error inside ProxyWorker` could join the exemption branch above. It is a transient network condition, not a configuration fault, and the code already makes that distinction for its two neighbours.

### Please provide any relevant error logs

Observed 8 times over 31 CI runs. The crash always lands on the same test, and the number of failures after it is identical every time. Never reproduced on a developer machine, where the same suite passes.

One data point that may help you place it: after splitting our suite so each job runs **one** server instead of two, the crash has not recurred — 2 runs, which we do not consider proof. That is consistent with contention producing the dropped connection. The dropped connection being **fatal** is the part being reported here.

Contributor guide

Open the contributing guide

Research direction

Trace the error path from ProxyWorker.js through cli.js, castErrorCause, ProxyController.emitErrorEvent, and DevEnv.handleErrorEvent. Start by checking how the posted error object is reconstructed and how the "Error inside ProxyWorker" reason reaches the fatal branch. Done means the underlying message is visible and a transient proxy connection failure no longer stops wrangler dev; the issue provides no named test file.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.