Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-websocket-econnreset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@cloudflare/vite-plugin": patch
---

Fix dev server crash on WebSocket client disconnect

When a WebSocket client disconnects while an upgrade request is being processed, the server would crash with an unhandled `ECONNRESET` error. The fix adds an error handler to the socket at the start of the upgrade process.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import http from "node:http";
import net from "node:net";
import { DeferredPromise, Miniflare, Response } from "miniflare";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { handleWebSocket } from "../websockets";
import type { AddressInfo } from "node:net";

describe("handleWebSocket", () => {
let httpServer: http.Server;
let miniflare: Miniflare;
let port: number;

beforeEach(async () => {
httpServer = http.createServer((_req, res) => res.end("OK"));
miniflare = new Miniflare({
modules: true,
script: `export default {
fetch() {
const [client, server] = Object.values(new WebSocketPair());
server.accept();
return new Response(null, { status: 101, webSocket: client });
}
}`,
});
handleWebSocket(httpServer, miniflare);
await new Promise<void>((r) => httpServer.listen(0, "127.0.0.1", r));
port = (httpServer.address() as AddressInfo).port;
});

afterEach(async () => {
await miniflare?.dispose();
await new Promise<void>((resolve, reject) =>
httpServer?.close((e) => (e ? reject(e) : resolve()))
);
});

// https://github.com/cloudflare/workers-sdk/issues/12047
test("survives client disconnect during upgrade", async () => {
// Mock dispatchFetch to simulate a slow response - the bug occurs when
// the client disconnects while dispatchFetch is pending
const deferred = new DeferredPromise<Response>();
vi.spyOn(miniflare, "dispatchFetch").mockReturnValue(deferred);

const socket = net.connect(port, "127.0.0.1");
await new Promise<void>((r) => socket.on("connect", r));
socket.write(
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" +
"Sec-WebSocket-Version: 13\r\n\r\n"
);

// Reset connection while dispatchFetch is pending, triggering ECONNRESET
socket.resetAndDestroy();

// Resolve the mock so miniflare.dispose() doesn't hang in afterEach
deferred.resolve(new Response(null));

// Verify server did not crash and is still responsive
const response = await fetch(`http://127.0.0.1:${port}`);
expect(response.ok).toBe(true);
});
});
3 changes: 3 additions & 0 deletions packages/vite-plugin-cloudflare/src/websockets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export function handleWebSocket(
httpServer.on(
"upgrade",
async (request: IncomingMessage, socket: Duplex, head: Buffer) => {
// Socket errors crash Node.js if unhandled
socket.on("error", () => socket.destroy());

const url = new URL(request.url ?? "", UNKNOWN_HOST);

// Ignore Vite HMR WebSockets
Expand Down
Loading