cloudflare / cloudflare/workers-sdk
[miniflare] dispatchFetch JSON-parses gzip-encoded ERROR_STACK 500 bodies
- 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?
- macOS (darwin 25.6.0), arm64
- Node v24.11.0
- miniflare 4.20260701.0 (via wrangler 4.107.0 / @cloudflare/vite-plugin 1.43.0)
- undici 7.28.0 (miniflare's copy)
On current `main`, `Miniflare#dispatchFetch` still JSON-parses the ERROR_STACK body (`JSON.parse(await response.text())` after #14766). That parse still fails if the body is gzip.
### Please provide a link to a minimal reproduction
https://github.com/cloudflare/vinext/issues/2921
### Describe the Bug
`Miniflare#dispatchFetch` treats a workerd 500 that carries `MF-Experimental-Error-Stack` as a structured exception and parses the body as JSON:
```js
const stack = response.headers.get(CoreHeaders.ERROR_STACK);
if (response.status === 500 && stack !== null) {
const caught = JsonErrorSchema.parse(await response.json());
throw reviveError(this.#workerSrcOpts, caught);
}
```
(`packages/miniflare/src/index.ts`, `dispatchFetch`. #14766 switched this to `response.text()` + `JSON.parse`, which has the same failure when the bytes are gzip.)
`DispatchFetchDispatcher` does not decompress. It forwards the caller's headers, including `Accept-Encoding: gzip`. workerd then returns the JSON error with `Content-Encoding: gzip`. `response.json()` / `JSON.parse(text)` therefore sees gzip magic `1f 8b 08` and throws:
```
SyntaxError: Unexpected token '\u001f', "\u001f�\b..." is not valid JSON
at JSON.parse
at parseJSONFromBytes (undici)
```
The `Content-Encoding` strip that follows this branch never runs, so the gzip is not rewritten to `MF-Content-Encoding` first.
This is the same `response.json()` fragility as #8917 (empty HEAD body). Here the body is present but compressed.
**How it shows up with `@cloudflare/vite-plugin`:** `handleWebSocket` copies every upgrade header via `createHeaders` (including the browser's `Accept-Encoding: gzip`) and `await miniflare.dispatchFetch(...)`. When the Worker throws on a WebSocket upgrade, this gzip parse rejects. On vite-plugin 1.43.0 that rejection is unhandled and kills the Node process. On current main, #14862 catches it and `socket.destroy()`s, so the process lives but the Worker's exception is never revived or logged. Related: #15170 (non-101 upgrade responses are destroyed).
**Expected:** decompress (or ignore `Accept-Encoding`) before parsing the ERROR_STACK body, then `reviveError` the Worker exception.
**Workaround on the vinext side:** absorb this gzip `SyntaxError` in the process-level socket-error backstop: https://github.com/cloudflare/vinext/pull/2922
### Please provide any relevant error logs
```
SyntaxError: Unexpected token '\u001f', "\u001f�\b..." is not valid JSON
at JSON.parse ()
at parseJSONFromBytes (undici/lib/web/infra/index.js)
at successSteps (undici/lib/web/fetch/body.js)
at Miniflare.dispatchFetch (miniflare/dist/src/index.js)
at Server. (@cloudflare/vite-plugin/dist/index.mjs) // handleWebSocket upgrade
```
Instrumented `Response.json()` on the failing call:
- status `500`
- `content-type: application/json`
- `content-encoding: gzip`
- body starts `1f8b0800000000000013` (gzip magic)
- `MF-Content-Encoding` is unset (the strip after the ERROR_STACK branch did not run)
Contributor guide
Research direction
Start in packages/miniflare/src/index.ts at Miniflare#dispatchFetch and inspect the ERROR_STACK handling alongside the Content-Encoding logic. Run the linked minimal reproduction or an equivalent gzip-encoded 500 response. Done means the compressed ERROR_STACK body is handled without a JSON parse failure and the Worker exception is revived.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100