Skip to content

[miniflare] dispatchFetch JSON-parses gzip-encoded ERROR_STACK 500 bodies #15198

Description

@quantizor

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

cloudflare/vinext#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:

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: cloudflare/vinext#2922

Please provide any relevant error logs

SyntaxError: Unexpected token '\u001f', "\u001f�\b..." is not valid JSON
    at JSON.parse (<anonymous>)
    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.<anonymous> (@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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions