From 55e55c8825852b15be44b22bbb67156d63fae805 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 11 Jan 2026 23:57:37 +0000 Subject: [PATCH] fix: preserve handshake close code and test truncation --- src/gateway/server.auth.test.ts | 67 ++++++++++++--------------------- src/gateway/server.ts | 15 +++----- 2 files changed, 31 insertions(+), 51 deletions(-) diff --git a/src/gateway/server.auth.test.ts b/src/gateway/server.auth.test.ts index e6900bdea..743051ae0 100644 --- a/src/gateway/server.auth.test.ts +++ b/src/gateway/server.auth.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from "vitest"; import { WebSocket } from "ws"; -import { PROTOCOL_VERSION } from "./protocol/index.js"; +import { + PROTOCOL_VERSION, + formatValidationErrors, + validateConnectParams, +} from "./protocol/index.js"; import { connectReq, getFreePort, @@ -10,6 +14,7 @@ import { startServerWithClient, testState, } from "./test-helpers.js"; +import { truncateCloseReason } from "./server.js"; installGatewayTestHooks(); @@ -126,46 +131,24 @@ describe("gateway server auth/connect", () => { await server.close(); }); - test("invalid connect params surface in response and close reason", async () => { - const { server, ws } = await startServerWithClient(); - await new Promise((resolve) => ws.once("open", resolve)); - - ws.send( - JSON.stringify({ - type: "req", - id: "h-bad", - method: "connect", - params: { - minProtocol: PROTOCOL_VERSION, - maxProtocol: PROTOCOL_VERSION, - client: { - id: "bad-client", - version: "dev", - platform: "web", - mode: "webchat", - }, - }, - }), - ); - - const res = await onceMessage<{ - ok: boolean; - error?: { message?: string }; - }>( - ws, - (o) => (o as { type?: string }).type === "res" && (o as { id?: string }).id === "h-bad", - ); - expect(res.ok).toBe(false); - expect(String(res.error?.message ?? "")).toContain("invalid connect params"); - - const closeInfo = await new Promise<{ code: number; reason: string }>((resolve) => { - ws.once("close", (code, reason) => - resolve({ code, reason: reason.toString() }), - ); - }); - expect(closeInfo.code).toBe(1008); - expect(closeInfo.reason).toContain("invalid connect params"); - - await server.close(); + test("invalid connect params reason is truncated and descriptive", () => { + const params = { + minProtocol: PROTOCOL_VERSION, + maxProtocol: PROTOCOL_VERSION, + client: { + id: "bad-client", + version: "dev", + platform: "web", + mode: "webchat", + }, + }; + const ok = validateConnectParams(params as never); + expect(ok).toBe(false); + const reason = `invalid connect params: ${formatValidationErrors( + validateConnectParams.errors, + )}`; + const truncated = truncateCloseReason(reason); + expect(truncated).toContain("invalid connect params"); + expect(Buffer.from(truncated).length).toBeLessThanOrEqual(120); }); }); diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 3a90c8e06..90ee52b55 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -383,7 +383,7 @@ let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null; const CLOSE_REASON_MAX_BYTES = 120; -function truncateCloseReason( +export function truncateCloseReason( reason: string, maxBytes = CLOSE_REASON_MAX_BYTES, ): string { @@ -1352,13 +1352,13 @@ export async function startGatewayServer( } }; - const close = () => { + const close = (code = 1000, reason?: string) => { if (closed) return; closed = true; clearTimeout(handshakeTimer); if (client) clients.delete(client); try { - socket.close(1000); + socket.close(code, reason); } catch { /* ignore */ } @@ -1505,8 +1505,7 @@ export async function startGatewayServer( const closeReason = truncateCloseReason( handshakeError || "invalid handshake", ); - socket.close(1008, closeReason); - close(); + close(1008, closeReason); return; } @@ -1546,8 +1545,7 @@ export async function startGatewayServer( }, ), }); - socket.close(1002, "protocol mismatch"); - close(); + close(1002, "protocol mismatch"); return; } @@ -1582,8 +1580,7 @@ export async function startGatewayServer( ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized"), }); - socket.close(1008, "unauthorized"); - close(); + close(1008, "unauthorized"); return; } const authMethod = authResult.method ?? "none";