fix: surface handshake reasons
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
- Docs: rename README “macOS app” section to “Apps”. (#733) — thanks @AbhisekBasu1.
|
- Docs: rename README “macOS app” section to “Apps”. (#733) — thanks @AbhisekBasu1.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Gateway/WebChat: include handshake validation details in the WebSocket close reason for easier debugging.
|
||||||
- Doctor: surface plugin diagnostics in the report.
|
- Doctor: surface plugin diagnostics in the report.
|
||||||
- CLI/Update: preserve base environment when passing overrides to update subprocesses. (#713) — thanks @danielz1z.
|
- 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.
|
- Agents: treat message tool errors as failures so fallback replies still send; require `to` + `message` for `action=send`. (#717) — thanks @theglove44.
|
||||||
|
|||||||
@@ -125,4 +125,47 @@ describe("gateway server auth/connect", () => {
|
|||||||
await new Promise<void>((resolve) => ws.once("close", () => resolve()));
|
await new Promise<void>((resolve) => ws.once("close", () => resolve()));
|
||||||
await server.close();
|
await server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("invalid connect params surface in response and close reason", async () => {
|
||||||
|
const { server, ws } = await startServerWithClient();
|
||||||
|
await new Promise<void>((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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Buffer } from "node:buffer";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { Server as HttpServer } from "node:http";
|
import type { Server as HttpServer } from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -380,6 +381,18 @@ let healthCache: HealthSummary | null = null;
|
|||||||
let healthRefresh: Promise<HealthSummary> | null = null;
|
let healthRefresh: Promise<HealthSummary> | null = null;
|
||||||
let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null;
|
let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null;
|
||||||
|
|
||||||
|
const CLOSE_REASON_MAX_BYTES = 120;
|
||||||
|
|
||||||
|
function truncateCloseReason(
|
||||||
|
reason: string,
|
||||||
|
maxBytes = CLOSE_REASON_MAX_BYTES,
|
||||||
|
): string {
|
||||||
|
if (!reason) return "invalid handshake";
|
||||||
|
const buf = Buffer.from(reason);
|
||||||
|
if (buf.length <= maxBytes) return reason;
|
||||||
|
return buf.subarray(0, maxBytes).toString();
|
||||||
|
}
|
||||||
|
|
||||||
function buildSnapshot(): Snapshot {
|
function buildSnapshot(): Snapshot {
|
||||||
const presence = listSystemPresence();
|
const presence = listSystemPresence();
|
||||||
const uptimeMs = Math.round(process.uptime() * 1000);
|
const uptimeMs = Math.round(process.uptime() * 1000);
|
||||||
@@ -1461,6 +1474,11 @@ export async function startGatewayServer(
|
|||||||
(parsed as RequestFrame).method !== "connect" ||
|
(parsed as RequestFrame).method !== "connect" ||
|
||||||
!validateConnectParams((parsed as RequestFrame).params)
|
!validateConnectParams((parsed as RequestFrame).params)
|
||||||
) {
|
) {
|
||||||
|
const handshakeError = validateRequestFrame(parsed)
|
||||||
|
? (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 (validateRequestFrame(parsed)) {
|
||||||
const req = parsed as RequestFrame;
|
const req = parsed as RequestFrame;
|
||||||
send({
|
send({
|
||||||
@@ -1469,9 +1487,7 @@ export async function startGatewayServer(
|
|||||||
ok: false,
|
ok: false,
|
||||||
error: errorShape(
|
error: errorShape(
|
||||||
ErrorCodes.INVALID_REQUEST,
|
ErrorCodes.INVALID_REQUEST,
|
||||||
req.method === "connect"
|
handshakeError,
|
||||||
? `invalid connect params: ${formatValidationErrors(validateConnectParams.errors)}`
|
|
||||||
: "invalid handshake: first request must be connect",
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1480,18 +1496,16 @@ export async function startGatewayServer(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
handshakeState = "failed";
|
handshakeState = "failed";
|
||||||
const handshakeError = validateRequestFrame(parsed)
|
|
||||||
? (parsed as RequestFrame).method === "connect"
|
|
||||||
? `invalid connect params: ${formatValidationErrors(validateConnectParams.errors)}`
|
|
||||||
: "invalid handshake: first request must be connect"
|
|
||||||
: "invalid request frame";
|
|
||||||
setCloseCause("invalid-handshake", {
|
setCloseCause("invalid-handshake", {
|
||||||
frameType,
|
frameType,
|
||||||
frameMethod,
|
frameMethod,
|
||||||
frameId,
|
frameId,
|
||||||
handshakeError,
|
handshakeError,
|
||||||
});
|
});
|
||||||
socket.close(1008, "invalid handshake");
|
const closeReason = truncateCloseReason(
|
||||||
|
handshakeError || "invalid handshake",
|
||||||
|
);
|
||||||
|
socket.close(1008, closeReason);
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user