From 8a135d29823ebea53d995b4c7f844e5548b01ee8 Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Thu, 21 May 2026 13:37:42 +0800 Subject: [PATCH] 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 --- packages/core/api/ws-client.test.ts | 21 +++++++++++++++++++++ packages/core/api/ws-client.ts | 16 +++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/core/api/ws-client.test.ts b/packages/core/api/ws-client.test.ts index 24e882021..2acd70b19 100644 --- a/packages/core/api/ws-client.test.ts +++ b/packages/core/api/ws-client.test.ts @@ -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(), diff --git a/packages/core/api/ws-client.ts b/packages/core/api/ws-client.ts index c5082be91..cc3d08e03 100644 --- a/packages/core/api/ws-client.ts +++ b/packages/core/api/ws-client.ts @@ -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") {