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:
- The browser WebSocket upgrade forwards
Accept-Encoding: gzip (vite-plugin createHeaders copies every header).
- workerd returns a 500 error JSON body with
Content-Encoding: gzip and MF-Experimental-Error-Stack.
- miniflare's
dispatchFetch always await response.json() on that branch. The custom DispatchFetchDispatcher does not decompress, so json() parses raw gzip bytes.
- The vite-plugin upgrade handler awaits
dispatchFetch with no catch. That is an unhandled rejection.
- 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
- App Router app on
vinext dev with @cloudflare/vite-plugin.
- The Worker handles WebSocket upgrades (any path the plugin does not skip as
sec-websocket-protocol: vite-*).
- A page that opens that socket after load.
- The Worker throws on the upgrade (any 500 that sets
MF-Experimental-Error-Stack).
- The browser sends
Accept-Encoding: gzip (default).
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.
What happens
vinext devexits the whole process when a Worker WebSocket upgrade returns a 500 whose error body is gzip-compressed JSON.The throw is this
SyntaxError(gzip magic1f 8b 08):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 thisSyntaxError, which takes downvinext 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:
dispatchFetchthrows 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:Chain:
Accept-Encoding: gzip(vite-plugincreateHeaderscopies every header).Content-Encoding: gzipandMF-Experimental-Error-Stack.dispatchFetchalwaysawait response.json()on that branch. The customDispatchFetchDispatcherdoes not decompress, sojson()parses raw gzip bytes.dispatchFetchwith nocatch. That is an unhandled rejection.The gzip parse itself lives in miniflare (
dispatchFetcharound theERROR_STACK/response.json()branch). The uncaughtupgradelistener 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 missingcatchon upgrade if that is a better home for (3) and (4).Env
node_modules/.pnpm/undici@7.28.0/...)Repro
vinext devwith@cloudflare/vite-plugin.sec-websocket-protocol: vite-*).MF-Experimental-Error-Stack).Accept-Encoding: gzip(default).vinext devexits with the gzipSyntaxErrorabove.Suggested fix
In the backstop, absorb this the way #1846 absorbed benign asset-import errors: a
SyntaxErrorfromparseJSONFromByteswhose 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) beforeresponse.json()on the 500/ERROR_STACKbranch, and the vite-plugin upgrade handler should catchdispatchFetchfailures and destroy the socket instead of leaving an unhandled rejection.