cloudflare / cloudflare/workers-sdk

`wrangler dev` exits with "Error inside ProxyWorker / Network connection lost" on POST requests with a body when `assets` is configured

Open
#15,203 4 comments 0 reactions 0 assignees View on GitHub
package:wrangler
Dominant language
TypeScript
Stars
4.5k
Forks
1.5k
Avg merge
3d 8h
Merged PRs (30d)
186

Description

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

```
System:
OS: Linux 6.8 Ubuntu 24.04.4 LTS 24.04.4 LTS (Noble Numbat)
CPU: (6) x64 AMD EPYC-Milan Processor
Memory: 13.01 GB / 15.61 GB
Container: Yes
Shell: 5.2.21 - /bin/bash
Binaries:
Node: 22.23.2 - /usr/bin/node
npm: 10.9.8 - /usr/bin/npm
pnpm: 9.0.0 - /usr/bin/pnpm
npmPackages:
wrangler: ^4.123.0 => 4.123.0
```

(workerd 1.20260811.1 and miniflare 5.20260811.1-alpha come in as wrangler's dependencies.)

### Please provide a link to a minimal reproduction

https://gist.github.com/June157/7dd408450d8c153b47986a8c3325132c

(5 files: `worker.mjs`, `wrangler.json`, `hammer.mjs`, `index.html` → put under `public/`, and a README with the two commands.)

### Describe the Bug

With Static Assets configured (`assets.directory`), `wrangler dev` **exits** — not just returns an error — after a few dozen POST requests that carry a body and target a path where an asset exists. The assets layer answers those requests with `405` itself (they never reach the user Worker), and after some number of them the ProxyWorker dies. The same POSTs **without a body** are fine indefinitely, and the same Worker **without `assets`** is fine indefinitely.

Client-side symptom: `405` … `405` → one or two `HTTP 500` → `socket hang up` / `ECONNRESET` → `ECONNREFUSED` for everything after. `wrangler dev` prints an empty `✘ [ERROR]` and exits.

Production Workers with the same code do not exhibit this.

**Minimal reproduction** (also in the gist)

`worker.mjs` — its content barely matters; the crashing requests never reach it:
```js
export default {
fetch(request) {
if (request.method === "POST") return new Response("Forbidden", { status: 403 });
return new Response("ok");
},
};
```

`wrangler.json`:
```json
{ "name": "repro", "main": "worker.mjs", "compatibility_date": "2026-08-13",
"assets": { "directory": "./public", "binding": "ASSETS" } }
```

`public/index.html`: any file (so that `/` is an asset path).

```bash
npx wrangler dev -c wrangler.json --port 8788
# in another shell — 300 POSTs with a small form body, 3 concurrent:
node --input-type=module -e '
const N=300,C=3,B="http://127.0.0.1:8788";let i=0,ok=0,fail=0;
const one=()=>fetch(B+"/",{method:"POST",body:"a=b",headers:{"content-type":"application/x-www-form-urlencoded"}});
await Promise.all(Array.from({length:C},async()=>{while(i=500)throw 0;ok++}catch{fail++}}}));
console.log({ok,fail});'
```

**Observed:** e.g. `{ ok: 107, fail: 193 }`; afterwards `curl http://127.0.0.1:8788/` → connection refused. Successful responses are `405` (from the assets layer, not from the Worker).

**Expected:** 300 × 405 (or the request forwarded to the Worker), and `wrangler dev` stays up.

**What I varied** (300 requests each, 2–3 runs each)

| Variation | Result |
| --- | --- |
| Same config, POST `/` **without** body | ✅ 300 × 405, alive |
| Same config, POST `/api/x` (no asset there) **with** body → reaches Worker → 403 | ✅ 300 × 403, alive |
| Same config, POST `/` with body, **concurrency 1** | ❌ dies (first failure at request 9) |
| **No `assets`** in config, POST `/` with body → 403 from Worker | ✅ alive |
| + `compatibility_flags: ["nodejs_compat"]` | no change either way |
| + `assets.html_handling: "drop-trailing-slash"` | no change |
| + `public/_headers` or `public/_redirects` | no change |
| Worker drains the body (`await request.text()`) before responding | irrelevant — request doesn't reach the Worker |

So the necessary conditions I could isolate are: **`assets` configured** + **POST with a body** + **path resolves to an asset** (assets layer answers 405).

**Where I hit it** (possibly related, not reduced)

In an Astro site on `@astrojs/cloudflare`, the same crash signature also occurs on `POST /api/contact` (an SSR route, *not* an asset) when the request has a form body and no `Origin` header — Astro's CSRF middleware answers 403 without reading the body. The identical request with an `Origin` header (handler reads the body, answers 400) never crashes, and the same request without a body never crashes. I could not reduce that variant to a bare Worker: a bare Worker returning 403 to the same request on a non-asset path stays up. I mention it because it may be the same underlying stream-handling issue reached by a different route; happy to provide the Astro repro if useful.

**Guess at the mechanism** (unverified)

With `assets`, requests pass through an asset router before the user Worker. When the router itself answers (405 for POST-to-asset) while a request body is still in flight, the abandoned body stream seems to surface as `Network connection lost` inside the ProxyWorker and is treated as fatal for the whole dev server rather than for that one request.

### Please provide any relevant error logs

Debug log (`~/.config/.wrangler/logs/wrangler-*.log`) at the moment of the crash:

```
Error in ProxyController: Error inside ProxyWorker
Error
at castErrorCause (…/wrangler/wrangler-dist/cli.js:179580:20)
at ProxyController2.emitErrorEvent (…/wrangler/wrangler-dist/cli.js:280188:20)
at ProxyController2.onProxyWorkerMessage (…/wrangler/wrangler-dist/cli.js:280065:18)
at PROXY_CONTROLLER (…/wrangler/wrangler-dist/cli.js:279792:24)
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
at async #handleLoopbackCustomFetchService (…/miniflare/dist/src/index.js:111517:22)
at async #handleLoopback (…/miniflare/dist/src/index.js:111748:20) {
cause: {
name: 'Error',
message: 'Network connection lost.',
stack: 'Error: Network connection lost.'
}
}
```

The user Worker's runtime WebSocket had opened ~2 s earlier; there is nothing between `Runtime.enable` and this error in the debug log.

Contributor guide

Open the contributing guide

Research direction

Run the reproduction from README using wrangler.json, worker.mjs, public/index.html, and the POST workload described in hammer.mjs. Start by tracing the wrangler dev ProxyWorker and assets request path around the reported Network connection lost error; done means repeated body-bearing POST requests to an asset return 405 or reach the Worker without terminating the dev server.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.