Skip to content

Dev server crashes with unhandled ECONNRESET when a WebSocket upgrade request lacks the vite-hmr/vite-ping subprotocol #23727

Closed

Description

@Znegil

Describe the bug

A WebSocket upgrade request to the Vite dev server without the vite-hmr (or vite-ping) subprotocol leaves the underlying socket without an error listener. When the client then resets the connection, the socket emits ECONNRESET with nothing listening, and the entire dev server process exits with an unhandled 'error' event.

Any stray WebSocket client — a port scanner, a misconfigured tool, or a first attempt at connecting to the HMR socket without knowing the subprotocol requirement — kills the dev server.

Root cause (analysis by @shinshin86)

This was first reported at shinshin86/mesh-avatar-studio#22, where @shinshin86 narrowed it down:

  1. Vite's dev server only handles WebSocket upgrade requests whose Sec-WebSocket-Protocol is vite-hmr or vite-ping. Any other upgrade request is left untouched, and no error listener is attached to its socket.
  2. When the client resets the connection, the socket emits ECONNRESET with nothing listening, and Node.js exits.
  3. They confirmed the crash with a bare Vite 7.3.6 project (only an index.html) on macOS / Node.js 22, and confirmed the same handling is still present in the latest Vite (8.3.4).

I independently reproduced it on Windows 11 / Node.js 24.15.0 (7.3.6) and Linux / Node.js 22.23.2 (bare 7.3.6 project, nothing else installed) — the crash is identical on all three platforms.

Reproduction

npm init -y && npm install vite@7.3.6
echo '<!doctype html><title>bare</title>' > index.html
npx vite --port 5199 --strictPort

Then, from another process, send a WebSocket upgrade without the subprotocol header and reset the connection:

import socket, struct, time
s = socket.create_connection(("127.0.0.1", 5199))
s.sendall(b"GET / HTTP/1.1\r\nHost: 127.0.0.1:5199\r\nUpgrade: websocket\r\n"
          b"Connection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
          b"Sec-WebSocket-Version: 13\r\n\r\n")  # no Sec-WebSocket-Protocol
time.sleep(1.0)
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
s.close()  # forces RST

A plain JS client also triggers it on Windows/macOS (process exit with a handshake-less socket sends RST there):

new WebSocket("ws://127.0.0.1:5199/"); // no subprotocol; kill the process after a few seconds

Control: connecting with the vite-hmr subprotocol works fine — the socket is owned by Vite's HMR layer and closes cleanly.

Logs

node:events:497
      throw er; // Unhandled 'error' event
      ^

Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:216:20)
Emitted 'error' event on Socket instance at:
    at emitErrorNT (node:internal/streams/destroy:170:8)
    at emitErrorCloseNT (node:internal/streams/destroy:129:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:89:21) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}

Expected behavior

The dev server should reject or ignore WebSocket upgrade requests that don't match its subprotocols — e.g. attach an error listener to unhandled upgrade sockets (or destroy them immediately) — instead of crashing the whole process.

System Info

vite: 7.3.6 (also present in 8.3.4 per @shinshin86's check)
Node.js: v24.15.0 (Windows 11), v22 (macOS), v22.23.2 (Linux)

Validations

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feat: devdev serverp2-edge-caseBug, but has workaround or limited in scope (priority)

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions