Repository navigation
fix(vite-plugin): don't mistake pipelined HTTP responses for claimed WebSockets #16516
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"; | ||
|
|
||
|
|
@@ -50,14 +49,12 @@ export function handleWebSocket( | |
|
|
||
| // 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. | ||
| // not route). `ws` attaches a `data` listener when it takes ownership, | ||
| // while an in-flight HTTP response on the same keep-alive connection | ||
| // only writes bytes, so listener count distinguishes the two. | ||
| // 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; | ||
| socket.destroyed || socket.listenerCount("data") > 0; | ||
|
Comment on lines
56
to
+57
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. 🟡 Passive listeners block Worker WebSockets When another upgrade listener observes socket data without answering, Learn moreAn upgrade listener can register a Example: A Vite extension attaches Recommended fix: Base ownership on evidence of another handler taking responsibility for this specific upgrade, not on the mere presence of a socket reader. Preserve the case where an ordinary HTTP response to an earlier pipelined request is still in flight. Was this helpful? React with 👍 or 👎 to provide feedback.
Comment on lines
56
to
+57
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. |
||
|
|
||
| try { | ||
| const rawHost = request.headers.host ?? UNKNOWN_HOST; | ||
|
|
||
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.
🔴 Raw upgrade owners lose their sockets
When another listener writes an upgrade response but uses
readableinstead ofdata,isClaimed()reports false. The plugin destroys that connection or sends a second handshake, depending on the Worker response.Learn more
The HTTP server sends each upgrade event to every registered listener. A listener can own the connection by writing an HTTP upgrade response and consuming subsequent input through
readable, without ever installing adatalistener. The new check then treats that socket as unclaimed in both the no-WebSocket and WebSocket branches, destroying an owned connection or issuing a second handshake. The previous write-count check caught these written handshakes but incorrectly counted pending pipelined HTTP responses.Example: A Vite plugin writes
HTTP/1.1 101 Switching Protocolsand registerssocket.on('readable', ...)for its protocol. If the Worker returns 404, this handler destroys the plugin's upgraded socket; if the Worker returns a WebSocket, it writes another 101.Recommended fix: Track ownership using a mechanism that can distinguish the upgrade listener's response from preceding pipelined HTTP responses, without relying exclusively on
datalisteners. Test an owner that writes its handshake and reads viareadablealongside the pipelined-response case.Was this helpful? React with 👍 or 👎 to provide feedback.