Skip to content

Commit 1e2aafc

Browse files
committed
fix(vite-plugin-cloudflare): handle socket errors during WebSocket upgrade
1 parent bbd8a5e commit 1e2aafc

File tree

3 files changed

+75
-0
lines changed

3 files changed

+75
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cloudflare/vite-plugin": patch
3+
---
4+
5+
Fix dev server crash on WebSocket client disconnect
6+
7+
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.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import http from "node:http";
2+
import net from "node:net";
3+
import { DeferredPromise, Miniflare, Response } from "miniflare";
4+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
5+
import { handleWebSocket } from "../websockets";
6+
import type { AddressInfo } from "node:net";
7+
8+
describe("handleWebSocket", () => {
9+
let httpServer: http.Server;
10+
let miniflare: Miniflare;
11+
let port: number;
12+
13+
beforeEach(async () => {
14+
httpServer = http.createServer((_req, res) => res.end("OK"));
15+
miniflare = new Miniflare({
16+
modules: true,
17+
script: `export default {
18+
fetch() {
19+
const [client, server] = Object.values(new WebSocketPair());
20+
server.accept();
21+
return new Response(null, { status: 101, webSocket: client });
22+
}
23+
}`,
24+
});
25+
handleWebSocket(httpServer, miniflare);
26+
await new Promise<void>((r) => httpServer.listen(0, "127.0.0.1", r));
27+
port = (httpServer.address() as AddressInfo).port;
28+
});
29+
30+
afterEach(async () => {
31+
await miniflare?.dispose();
32+
await new Promise<void>((resolve, reject) =>
33+
httpServer?.close((e) => (e ? reject(e) : resolve()))
34+
);
35+
});
36+
37+
// https://github.com/cloudflare/workers-sdk/issues/12047
38+
test("survives client disconnect during upgrade", async () => {
39+
// Mock dispatchFetch to simulate a slow response - the bug occurs when
40+
// the client disconnects while dispatchFetch is pending
41+
const deferred = new DeferredPromise<Response>();
42+
vi.spyOn(miniflare, "dispatchFetch").mockReturnValue(deferred);
43+
44+
const socket = net.connect(port, "127.0.0.1");
45+
await new Promise<void>((r) => socket.on("connect", r));
46+
socket.write(
47+
"GET / HTTP/1.1\r\n" +
48+
"Host: localhost\r\n" +
49+
"Upgrade: websocket\r\n" +
50+
"Connection: Upgrade\r\n" +
51+
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" +
52+
"Sec-WebSocket-Version: 13\r\n\r\n"
53+
);
54+
55+
// Reset connection while dispatchFetch is pending, triggering ECONNRESET
56+
socket.resetAndDestroy();
57+
58+
// Resolve the mock so miniflare.dispose() doesn't hang in afterEach
59+
deferred.resolve(new Response(null));
60+
61+
// Verify server did not crash and is still responsive
62+
const response = await fetch(`http://127.0.0.1:${port}`);
63+
expect(response.ok).toBe(true);
64+
});
65+
});

packages/vite-plugin-cloudflare/src/websockets.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export function handleWebSocket(
2020
httpServer.on(
2121
"upgrade",
2222
async (request: IncomingMessage, socket: Duplex, head: Buffer) => {
23+
// Socket errors crash Node.js if unhandled
24+
socket.on("error", () => socket.destroy());
25+
2326
const url = new URL(request.url ?? "", UNKNOWN_HOST);
2427

2528
// Ignore Vite HMR WebSockets

0 commit comments

Comments
 (0)