Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit b0bb715

Browse files
committed
Allow WebSocket client connection errors to be caught, closes #229
Our behaviour is still different from the Workers runtime here. If a `fetch` with `Upgrade: websocket` is sent, and a connection cannot be established, the `fetch` will return a `502 Bad Gateway` `Response`, with `Response#webSocket` set to `undefined`.
1 parent 9f4ba28 commit b0bb715

File tree

3 files changed

+40
-32
lines changed

3 files changed

+40
-32
lines changed

packages/web-sockets/src/couple.ts

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { once } from "events";
12
import { viewToBuffer } from "@miniflare/shared";
23
import StandardWebSocket from "ws";
34
import {
@@ -7,6 +8,7 @@ import {
78
kClose,
89
kClosed,
910
kCoupled,
11+
kSend,
1012
} from "./websocket";
1113

1214
export async function coupleWebSocket(
@@ -24,20 +26,16 @@ export async function coupleWebSocket(
2426
);
2527
}
2628

27-
// Forward events from client to worker
29+
// Forward messages from client to worker (register this before `open` to
30+
// ensure messages queued before other pair `accept`s to release)
2831
ws.on("message", (message: Buffer, isBinary: boolean) => {
29-
if (isBinary) {
30-
pair.send(viewToBuffer(message));
31-
} else {
32-
pair.send(message.toString());
32+
// Silently discard messages sent after close
33+
if (!pair[kClosed]) {
34+
// Convert binary messages to `ArrayBuffer`s (note `[kSend]` will queue
35+
// messages if other pair hasn't `accept`ed yet)
36+
pair[kSend](isBinary ? viewToBuffer(message) : message.toString());
3337
}
3438
});
35-
ws.on("close", (code: number, reason: Buffer) => {
36-
if (!pair[kClosed]) pair[kClose](code, reason.toString());
37-
});
38-
ws.on("error", (error) => {
39-
pair.dispatchEvent(new ErrorEvent("error", { error }));
40-
});
4139

4240
// Forward events from worker to client
4341
pair.addEventListener("message", (e) => {
@@ -55,26 +53,24 @@ export async function coupleWebSocket(
5553

5654
if (ws.readyState === StandardWebSocket.CONNECTING) {
5755
// Wait for client to be open before accepting worker pair (which would
58-
// release buffered messages)
59-
await new Promise<void>((resolve, reject) => {
60-
ws.once("open", () => {
61-
pair.accept();
62-
pair[kCoupled] = true;
63-
64-
ws.off("close", reject);
65-
ws.off("error", reject);
66-
resolve();
67-
});
68-
ws.once("close", reject);
69-
ws.once("error", reject);
70-
});
71-
} else {
72-
// Accept worker pair immediately
73-
pair.accept();
74-
pair[kCoupled] = true;
75-
// Throw error if socket is already closing/closed
76-
if (ws.readyState >= StandardWebSocket.CLOSING) {
77-
throw new TypeError("Incoming WebSocket connection already closed.");
78-
}
56+
// release buffered messages). Note this will throw if an "error" event is
57+
// dispatched.
58+
await once(ws, "open");
59+
} else if (ws.readyState >= StandardWebSocket.CLOSING) {
60+
throw new TypeError("Incoming WebSocket connection already closed.");
7961
}
62+
pair.accept();
63+
pair[kCoupled] = true;
64+
65+
// Forward close/error events from client to worker (register this after
66+
// `once(ws, "open")` to ensure close/error due to connection failure throws
67+
// and can be caught from this function: https://github.com/cloudflare/miniflare/issues/229)
68+
ws.on("close", (code: number, reason: Buffer) => {
69+
// `[kClose]` skips code/reason validation, allowing reserved codes
70+
// (e.g. 1005 for "No Status Received")
71+
if (!pair[kClosed]) pair[kClose](code, reason.toString());
72+
});
73+
ws.on("error", (error) => {
74+
pair.dispatchEvent(new ErrorEvent("error", { error }));
75+
});
8076
}

packages/web-sockets/src/websocket.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ const kPair = Symbol("kPair");
5050
export const kAccepted = Symbol("kAccepted");
5151
export const kCoupled = Symbol("kCoupled");
5252
export const kClosed = Symbol("kClosed");
53+
54+
// Internal send method exposed to bypass accept checking
55+
export const kSend = Symbol("kSend");
5356
// Internal close method exposed to bypass close code checking
5457
export const kClose = Symbol("kClose");
5558

@@ -121,6 +124,10 @@ export class WebSocket extends InputGatedEventTarget<WebSocketEventMap> {
121124
"You must call accept() on this WebSocket before sending messages."
122125
);
123126
}
127+
this[kSend](message);
128+
}
129+
130+
[kSend](message: ArrayBuffer | string): void {
124131
if (this[kClosed]) {
125132
throw new TypeError("Can't call WebSocket send() after close().");
126133
}

packages/web-sockets/test/fetch.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ test("upgradingFetch: requires GET for web socket upgrade", async (t) => {
200200
}
201201
);
202202
});
203+
test("upgradeFetch: throws catchable error on connection failure", async (t) => {
204+
await t.throwsAsync(
205+
upgradingFetch("http://[100::]", { headers: { upgrade: "websocket" } })
206+
);
207+
});
203208
test("upgradingFetch: increments subrequest count", async (t) => {
204209
const server = await useServer(
205210
t,

0 commit comments

Comments
 (0)