fix(ws): truncate unparseable frame payload in client warn log (#2974)

The post-#2946 onmessage guard logs the raw event.data alongside the
warning. A malformed or rogue server can stream arbitrarily large
garbage and bloat the renderer / desktop main-process log buffers, so
cap the logged payload to the first 200 chars and append a
"(truncated, N chars total)" suffix when truncation occurs.

MUL-2490

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Bohan Jiang
2026-05-21 13:37:42 +08:00
committed by GitHub
parent 83e90c9530
commit 8a135d2982
2 changed files with 36 additions and 1 deletions

View File

@@ -73,6 +73,27 @@ describe("WSClient", () => {
expect(url.searchParams.has("client_os")).toBe(false);
});
it("truncates the logged payload when an unparseable frame is large", () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const ws = new WSClient("ws://example.test/ws", { logger });
ws.connect();
const huge = "x".repeat(5000);
FakeWebSocket.lastInstance!.onmessage?.({ data: huge });
expect(logger.warn).toHaveBeenCalledTimes(1);
const [, summary] = logger.warn.mock.calls[0] as [string, string];
expect(summary.length).toBeLessThan(huge.length);
expect(summary).toContain("truncated");
expect(summary).toContain("5000");
expect(summary.startsWith("x".repeat(200))).toBe(true);
});
it("logs and skips malformed frames without breaking later messages", () => {
const logger = {
debug: vi.fn(),

View File

@@ -3,6 +3,17 @@ import { type Logger, noopLogger } from "../logger";
type EventHandler = (payload: unknown, actorId?: string, actorType?: string) => void;
// Cap how much of an unparseable frame we put into the log. A malformed or
// rogue server can stream arbitrarily large garbage, and the warn handler may
// be a console / IPC bridge whose buffers we don't want to blow.
const UNPARSEABLE_LOG_MAX_CHARS = 200;
function summarizeUnparseable(data: unknown): string {
const text = typeof data === "string" ? data : String(data);
if (text.length <= UNPARSEABLE_LOG_MAX_CHARS) return text;
return `${text.slice(0, UNPARSEABLE_LOG_MAX_CHARS)}… (truncated, ${text.length} chars total)`;
}
/** Identifies the WS client to the server. Sent as `client_platform`,
* `client_version`, and `client_os` query parameters on the upgrade URL —
* browsers cannot set custom headers on WebSocket handshakes, so query
@@ -79,7 +90,10 @@ export class WSClient {
try {
msg = JSON.parse(event.data as string) as WSMessage;
} catch {
this.logger.warn("ws: received unparseable message", event.data);
this.logger.warn(
"ws: received unparseable message",
summarizeUnparseable(event.data),
);
return;
}
if ((msg as any).type === "auth_ack") {