diff --git a/ui/src/api/gateway-browser-socket.test.ts b/ui/src/api/gateway-browser-socket.test.ts index 2391e3d19f50..9809b0b93453 100644 --- a/ui/src/api/gateway-browser-socket.test.ts +++ b/ui/src/api/gateway-browser-socket.test.ts @@ -76,7 +76,20 @@ describe("createBrowserGatewaySocket", () => { socket?.emit("error"); socket?.emit("close", { code: 1006, reason: "" }); expect(handlers.error).toHaveBeenCalledOnce(); - expect(handlers.close).toHaveBeenCalledWith(1006, ""); + expect(handlers.close).toHaveBeenCalledWith( + 1006, + `gateway websocket opening timed out after ${DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS}ms`, + ); + }); + + it("preserves a real close reason when an opening timeout also occurred", async () => { + const handlers = createHandlers(); + createBrowserGatewaySocket("wss://gateway.example", handlers); + + await vi.advanceTimersByTimeAsync(DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS); + sockets[0]?.emit("close", { code: 1006, reason: "gateway supplied a close reason" }); + + expect(handlers.close).toHaveBeenCalledWith(1006, "gateway supplied a close reason"); }); it("clears the opening deadline after the socket opens", async () => { diff --git a/ui/src/api/gateway-browser-socket.ts b/ui/src/api/gateway-browser-socket.ts index 2efc0e55d0ca..e93c7f93ac37 100644 --- a/ui/src/api/gateway-browser-socket.ts +++ b/ui/src/api/gateway-browser-socket.ts @@ -10,7 +10,7 @@ export function createBrowserGatewaySocket( ): GatewayProtocolSocket { const socket = new WebSocket(url); let opening = true; - let openingTimedOut = false; + let openingTimeoutReason: string | undefined; let openingTimer: ReturnType | undefined; const finishOpening = () => { opening = false; @@ -27,11 +27,12 @@ export function createBrowserGatewaySocket( socket.addEventListener("message", (event) => handlers.message(String(event.data ?? ""))); socket.addEventListener("close", (event) => { finishOpening(); - handlers.close(event.code, event.reason ?? ""); + // Browsers erase locally initiated close reasons before the handshake finishes. + handlers.close(event.code, event.reason || openingTimeoutReason || ""); }); socket.addEventListener("error", () => { finishOpening(); - if (!openingTimedOut) { + if (!openingTimeoutReason) { handlers.error(new Error("websocket error")); } }); @@ -44,13 +45,9 @@ export function createBrowserGatewaySocket( return; } opening = false; - openingTimedOut = true; + openingTimeoutReason = `gateway websocket opening timed out after ${DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS}ms`; try { - handlers.error( - new Error( - `gateway websocket opening timed out after ${DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS}ms`, - ), - ); + handlers.error(new Error(openingTimeoutReason)); } finally { socket.close(); }