Skip to content

Dev server exits on gzipped Worker 500 during WebSocket upgrade (socket-error-backstop rethrows undici JSON parse) #2921

Description

@quantizor

What happens

vinext dev exits the whole process when a Worker WebSocket upgrade returns a 500 whose error body is gzip-compressed JSON.

The throw is this SyntaxError (gzip magic 1f 8b 08):

SyntaxError: Unexpected token '\u001f', "\u001f�\b\u0000"... is not valid JSON
    at JSON.parse (<anonymous>)
    at parseJSONFromBytes (undici/lib/web/infra/index.js)
    at successSteps (undici/lib/web/fetch/body.js)

vinext's process-level backstop (dist/server/socket-error-backstop.js) only absorbs peer-disconnect codes (ECONNRESET / EPIPE / ECONNABORTED) and asset-import noise. It rethrows this SyntaxError, which takes down vinext dev.

When

Reliably about a second after a page loads that opens a Worker WebSocket, and again after HMR when that socket reconnects. Health checks keep returning 200 until the throw. Nothing in the Vite request log shows the failed upgrade: dispatchFetch throws before the plugin can log it.

Why I think it is a vinext bug

I instrumented undici's Response.json() at the parse site. The failing call is:

status: 500
content-type: application/json
content-encoding: gzip
body: 752 bytes starting 1f8b0800000000000013  (gzip)

at _Response.json (undici/lib/web/fetch/body.js)
at _Miniflare.dispatchFetch (miniflare/dist/src/index.js)   // `await response.json()` when status === 500 && MF-Experimental-Error-Stack is set
at async Server.<anonymous> (@cloudflare/vite-plugin/dist/index.mjs)  // httpServer 'upgrade' handler, no try/catch

Chain:

  1. The browser WebSocket upgrade forwards Accept-Encoding: gzip (vite-plugin createHeaders copies every header).
  2. workerd returns a 500 error JSON body with Content-Encoding: gzip and MF-Experimental-Error-Stack.
  3. miniflare's dispatchFetch always await response.json() on that branch. The custom DispatchFetchDispatcher does not decompress, so json() parses raw gzip bytes.
  4. The vite-plugin upgrade handler awaits dispatchFetch with no catch. That is an unhandled rejection.
  5. vinext's backstop rethrows, and the process exits.

The gzip parse itself lives in miniflare (dispatchFetch around the ERROR_STACK / response.json() branch). The uncaught upgrade listener lives in @cloudflare/vite-plugin. I am filing here because vinext is what turns that rejection into process death, the same way #905 / #913 / #1846 absorbed other non-fatal uncaughts. Next.js's equivalent handler logs and keeps serving.

Related: cloudflare/workers-sdk#8917 (same dispatchFetch + response.json() path, empty body on HEAD instead of gzip). I can file a workers-sdk issue for the gzip parse and the missing catch on upgrade if that is a better home for (3) and (4).

Env

  • vinext: 1.0.0-beta.4
  • @cloudflare/vite-plugin: 1.43.0
  • wrangler: 4.107.0
  • miniflare: 4.20260701.0
  • undici: 7.28.0 (miniflare's copy, not Node's bundled fetch: the stack is node_modules/.pnpm/undici@7.28.0/...)
  • Node: v24.11.0
  • OS: macOS (darwin 25.6.0)

Repro

  1. App Router app on vinext dev with @cloudflare/vite-plugin.
  2. The Worker handles WebSocket upgrades (any path the plugin does not skip as sec-websocket-protocol: vite-*).
  3. A page that opens that socket after load.
  4. The Worker throws on the upgrade (any 500 that sets MF-Experimental-Error-Stack).
  5. The browser sends Accept-Encoding: gzip (default).
  6. vinext dev exits with the gzip SyntaxError above.

Suggested fix

In the backstop, absorb this the way #1846 absorbed benign asset-import errors: a SyntaxError from parseJSONFromBytes whose first byte is gzip (0x1f 0x8b) is not a vinext bug, and killing the process loses the original Worker 500.

The durable fix is still upstream: miniflare should decompress (or text() + gunzip) before response.json() on the 500/ERROR_STACK branch, and the vite-plugin upgrade handler should catch dispatchFetch failures and destroy the socket instead of leaving an unhandled rejection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions