Repository navigation
[vite-plugin] Route v1 DevTools WebSocket upgrades explicitly #16538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ import { UNKNOWN_HOST } from "./shared"; | |
| import { createHeadersForIncomingMessage, getForwardedProto } from "./utils"; | ||
| import type { Headers, Miniflare } from "miniflare"; | ||
| import type { IncomingMessage } from "node:http"; | ||
| import type { Socket } from "node:net"; | ||
| import type { Duplex } from "node:stream"; | ||
| import type * as vite from "vite"; | ||
|
|
||
|
|
@@ -39,26 +38,12 @@ export function handleWebSocket( | |
| } | ||
| ); | ||
|
|
||
| // Prepend so the write-count baseline below is captured before another | ||
| // `upgrade` listener can answer the same request. Node dispatches `upgrade` | ||
| // to every listener, not just the first. | ||
| httpServer.prependListener( | ||
| httpServer.on( | ||
| "upgrade", | ||
| async (request: IncomingMessage, socket: Duplex, head: Buffer) => { | ||
| // Socket errors crash Node.js if unhandled | ||
| socket.on("error", () => socket.destroy()); | ||
|
|
||
| // Another listener may already have completed the handshake (for | ||
| // example Vite DevTools at `/__devtools/__ws`, which the Worker does | ||
| // not route). `bytesWritten` is cumulative across keep-alive | ||
| // connections, so compare against a baseline rather than zero. | ||
| // See https://github.com/cloudflare/workers-sdk/issues/15654 | ||
| const bytesWrittenAtStart = | ||
| (socket as unknown as Socket).bytesWritten ?? 0; | ||
| const isClaimed = () => | ||
| socket.destroyed || | ||
| ((socket as unknown as Socket).bytesWritten ?? 0) > bytesWrittenAtStart; | ||
|
|
||
| try { | ||
| const rawHost = request.headers.host ?? UNKNOWN_HOST; | ||
| // Honor `X-Forwarded-Proto` so that the upgrade URL reflects the | ||
|
|
@@ -72,10 +57,12 @@ export function handleWebSocket( | |
|
|
||
| const isViteRequest = | ||
| request.headers["sec-websocket-protocol"]?.startsWith("vite"); | ||
| const isDevToolsRequest = url.pathname === "/__devtools/__ws"; | ||
| const isSandboxRequest = hasSandboxOrigin(url.origin); | ||
|
|
||
| // Ignore Vite HMR WebSockets but forward on all sandbox requests. | ||
| if (isViteRequest && !isSandboxRequest) { | ||
| // Leave Vite HMR and DevTools upgrades to their own handlers, but | ||
| // forward all sandbox requests to the Worker. | ||
| if ((isViteRequest || isDevToolsRequest) && !isSandboxRequest) { | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -92,17 +79,11 @@ export function handleWebSocket( | |
| const workerWebSocket = response.webSocket; | ||
|
|
||
| if (!workerWebSocket) { | ||
| // This listener does not own the socket. Destroy it only if no | ||
| // other listener has written a response in the meantime. | ||
| if (!isClaimed()) { | ||
| socket.destroy(); | ||
| } | ||
| socket.destroy(); | ||
|
Comment on lines
81
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Other plugins' WebSocket connections break When another listener upgrades a non-DevTools path, Learn moreNode invokes every Example: A plugin accepts Recommended fix: Restore an ownership check for non-reserved paths before destroying or upgrading a socket. Distinguish an earlier HTTP response on a pipelined connection from another listener's handshake, since the removed byte-count check rejected valid Worker upgrades in that case. Test a second listener on a non-DevTools path with both Worker response types. Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| return; | ||
| } | ||
|
|
||
| // A listener claimed the socket while `dispatchFetch` was in | ||
| // flight, so a second upgrade would corrupt the connection. | ||
| if (isClaimed()) { | ||
| if (socket.destroyed) { | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -129,14 +110,9 @@ export function handleWebSocket( | |
| // This listener is `async`, so an uncaught rejection here escapes as | ||
| // an unhandled rejection — which terminates the Node.js process on | ||
| // modern versions and leaks the client socket. Tear the socket down | ||
| // instead. Defer the teardown one tick so another listener can claim | ||
| // the socket first, mirroring the `!workerWebSocket` path above. | ||
| // instead. | ||
| workerResponseHeaders.delete(request); | ||
| setImmediate(() => { | ||
| if (!isClaimed()) { | ||
| socket.destroy(); | ||
| } | ||
| }); | ||
| socket.destroy(); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Preview WebSocket route hangs without a handler
In preview,
handleWebSocketskips/__devtools/__wsdespite no DevTools listener handling that path. A Worker WebSocket at that path never receives the upgrade, leaving its client waiting.Learn more
The same
handleWebSocketfunction is installed by both devPlugin and previewPlugin. A preview server does not have the Vite DevTools handler that this pathname exemption expects. Returning from the only relevant upgrade listener does not send an HTTP response or close the socket. Previously the Worker received this request and could return a WebSocket.Example: A Worker handles
/__devtools/__wswith a WebSocket response. Connecting to that path onvite previewnow receives neither a 101 response nor the Worker's messages.Recommended fix: Scope the DevTools exemption to development servers where the DevTools upgrade listener is actually present, and preserve Worker forwarding in preview. Add a preview-specific upgrade test.
Was this helpful? React with 👍 or 👎 to provide feedback.