Skip to content

fix(vite-plugin): deliver non-101 responses on rejected WebSocket upgrades - #15205

Open
patrickswedish wants to merge 1 commit into
cloudflare:mainfrom
patrickswedish:fix/vite-plugin-ws-rejection-response
Open

fix(vite-plugin): deliver non-101 responses on rejected WebSocket upgrades#15205
patrickswedish wants to merge 1 commit into
cloudflare:mainfrom
patrickswedish:fix/vite-plugin-ws-rejection-response

Conversation

@patrickswedish

@patrickswedish patrickswedish commented Aug 15, 2026

Copy link
Copy Markdown

Summary

Fixes #15170.

When a Worker rejects a WebSocket upgrade request with a non-101 HTTP response (such as 401 Unauthorized or 403 Forbidden), the Vite dev server's handleWebSocket listener previously saw response.webSocket === undefined and called socket.destroy() unconditionally. This dropped the underlying socket without transmitting any HTTP response line, headers, or body, violating RFC 6455 §4.1 and causing native WebSocket clients to fail with abrupt socket hang-ups rather than handling the authentication challenge.

Root Cause

In packages/vite-plugin-cloudflare/src/websockets.ts (handleWebSocket), the upgrade listener only implemented the 101 WebSocket upgrade path. Non-101 responses were treated as unhandled failures and destroyed the socket directly:

const workerWebSocket = response.webSocket;
if (!workerWebSocket) {
    socket.destroy();
    return;
}

What Changed

  • HTTP Serialization & Delivery: When !workerWebSocket, serialize the Worker's Response directly to the raw upgrade socket via dedicated writeHttpResponse().
  • Status & Headers: Forwards HTTP status code, status text, and response headers (with dedicated multi-value Set-Cookie support). Defaults Connection: close if not specified.
  • Backpressure Support: Body streaming honors TCP backpressure via waitForSocketDrain(), which resolves on drain, close, or error events to prevent unbounded buffering or indefinite hangs if remote peers disconnect.
  • Resource & Lifecycle Cleanup: Always cleans up event listeners on drain/close/error and releases reader locks in a finally block (reader.releaseLock()).
  • Orderly Teardown: Closes the socket gracefully with socket.end() rather than unconditionally invoking socket.destroy().
  • Regression Tests: Added comprehensive unit tests in packages/vite-plugin-cloudflare/src/__tests__/websockets.spec.ts testing non-101 responses (401/403/etc.), header forwarding, body streaming, backpressure handling, client disconnect resilience, and listener cleanup.
  • Changeset: Added patch changeset for @cloudflare/vite-plugin.

Validation

  • Tests included/updated

  • Documentation not necessary because: bug fix restoring standard RFC 6455 behavior in dev server

  • Tested with Vitest in packages/vite-plugin-cloudflare (websockets.spec.ts).

  • Verified 401/403 status, custom headers (X-Custom-Auth), and body delivery on WebSocket upgrade rejections.

  • Verified backpressure handling and client disconnect resilience without hanging workers or unhandled rejections.

  • Verified successful 101 WebSocket upgrades continue to work with header forwarding.

Compatibility / Risk

Zero risk to production runtime (this code only runs in @cloudflare/vite-plugin local dev mode). Non-101 upgrade responses now adhere to standard RFC 6455 §4.1 behavior.


Devin Review

@changeset-bot

changeset-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2df0107

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/vite-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Aug 15, 2026
@workers-devprod
workers-devprod requested review from a team and edmundhung and removed request for a team August 15, 2026 10:03
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/vite-plugin-ws-rejection-response.md: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/tests/websockets.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/websockets.ts: [@cloudflare/wrangler]

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@pkg-pr-new

pkg-pr-new Bot commented Aug 15, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15205

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15205

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15205

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15205

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15205

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15205

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15205

miniflare

npm i https://pkg.pr.new/miniflare@15205

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15205

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15205

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15205

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15205

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15205

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15205

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15205

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15205

wrangler

npm i https://pkg.pr.new/wrangler@15205

commit: 2df0107

devin-ai-integration[bot]

This comment was marked as resolved.

@edmundhung edmundhung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment from Devin looks valid. Can you take a look? I think the CI error also looks legit.

@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/vite-plugin-ws-rejection-response.md: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/tests/websockets.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/websockets.ts: [@cloudflare/wrangler]

@patrickswedish

Copy link
Copy Markdown
Author

Thanks @edmundhung! That review observation was spot on.

Summary of Fix

  1. Graceful Response Drainage & Orderly EOF: Replaced the premature socket destruction and detached \ServerResponse\ approach with a dedicated \writeHttpResponse\ stream handler. It streams status line, headers (preserving custom headers and cookies), and the response body directly to the upgrade socket with backpressure handling (\socket.once('drain')) and signals clean completion via \socket.end()\ (graceful TCP FIN) instead of hard reset (\socket.destroy()).
  2. Windows \ECONNRESET\ Elimination: Cleanly manages socket lifecycle during WebSocket upgrade rejections without leaving dangling/detached connection state that triggered Windows Winsock resets.
  3. Regression Hardening: Extended the rejection test in \packages/vite-plugin-cloudflare/src/tests/websockets.spec.ts\ to verify complete delivery of a multi-frame 64 KB response body and assert orderly connection EOF/closure.

devin-ai-integration[bot]

This comment was marked as resolved.

@patrickswedish
patrickswedish force-pushed the fix/vite-plugin-ws-rejection-response branch from 74003c1 to 519c031 Compare August 29, 2026 13:40
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/vite-plugin-ws-rejection-response.md: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/tests/websockets.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/websockets.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

…rades

Fix WebSocket upgrade handling in Vite plugin dev mode so non-101 HTTP
responses (such as 401 Unauthorized or 403 Forbidden) returned by Workers
are properly written to the raw socket with backpressure and stream cleanup
before terminating, rather than abruptly destroying the socket.

Fixes cloudflare#15170
@patrickswedish
patrickswedish force-pushed the fix/vite-plugin-ws-rejection-response branch from 519c031 to 2df0107 Compare August 30, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

[vite-plugin] Non-101 WebSocket upgrade responses are destroyed instead of delivered to the client in dev

3 participants