diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea1fd8c6..51b822391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ### Fixes - Gateway/WebChat: include handshake validation details in the WebSocket close reason for easier debugging. +- Gateway/Auth: send invalid connect responses before closing the handshake; stabilize invalid-connect auth test. - Doctor: surface plugin diagnostics in the report. - CLI/Update: preserve base environment when passing overrides to update subprocesses. (#713) — thanks @danielz1z. - Agents: treat message tool errors as failures so fallback replies still send; require `to` + `message` for `action=send`. (#717) — thanks @theglove44. diff --git a/src/gateway/server.auth.test.ts b/src/gateway/server.auth.test.ts index 93158b6d6..5d766d65d 100644 --- a/src/gateway/server.auth.test.ts +++ b/src/gateway/server.auth.test.ts @@ -126,20 +126,11 @@ describe("gateway server auth/connect", () => { await server.close(); }); - test.skip( + test( "invalid connect params surface in response and close reason", { timeout: 15000 }, async () => { const { server, ws } = await startServerWithClient(); - await new Promise((resolve) => ws.once("open", resolve)); - - const closePromise = new Promise<{ code: number; reason: string }>( - (resolve) => { - ws.once("close", (code, reason) => - resolve({ code, reason: reason.toString() }), - ); - }, - ); ws.send( JSON.stringify({ @@ -159,37 +150,34 @@ describe("gateway server auth/connect", () => { }), ); - const raceResult = await Promise.race([ - onceMessage<{ - ok: boolean; - error?: { message?: string }; - }>( - ws, - (o) => - (o as { type?: string }).type === "res" && - (o as { id?: string }).id === "h-bad", - ), - closePromise, - ]); + 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", + ); - if ("ok" in raceResult) { - expect(raceResult.ok).toBe(false); - expect(String(raceResult.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"); - } else { - // handshake timed out/closed before response; still ensure closure happened - expect(raceResult.code === 1008 || raceResult.code === 1000).toBe(true); - } + const closeInfo = await new Promise<{ code: number; reason: string }>( + (resolve, reject) => { + const timer = setTimeout( + () => reject(new Error("close timeout")), + 3000, + ); + ws.once("close", (code, reason) => { + clearTimeout(timer); + resolve({ code, reason: reason.toString() }); + }); + }, + ); + expect(closeInfo.code).toBe(1008); + expect(closeInfo.reason).toContain("invalid connect params"); await server.close(); }, diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 9c93b95f6..aa3dba9c9 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -1469,17 +1469,18 @@ export async function startGatewayServer( if (!client) { // Handshake must be a normal request: // { type:"req", method:"connect", params: ConnectParams }. + const isRequestFrame = validateRequestFrame(parsed); if ( - !validateRequestFrame(parsed) || + !isRequestFrame || (parsed as RequestFrame).method !== "connect" || !validateConnectParams((parsed as RequestFrame).params) ) { - const handshakeError = validateRequestFrame(parsed) + const handshakeError = isRequestFrame ? (parsed as RequestFrame).method === "connect" ? `invalid connect params: ${formatValidationErrors(validateConnectParams.errors)}` : "invalid handshake: first request must be connect" : "invalid request frame"; - if (validateRequestFrame(parsed)) { + if (isRequestFrame) { const req = parsed as RequestFrame; send({ type: "res", @@ -1502,7 +1503,11 @@ export async function startGatewayServer( const closeReason = truncateCloseReason( handshakeError || "invalid handshake", ); - close(1008, closeReason); + if (isRequestFrame) { + queueMicrotask(() => close(1008, closeReason)); + } else { + close(1008, closeReason); + } return; }