diff --git a/extensions/googlechat/src/api.fetchok.transport.test.ts b/extensions/googlechat/src/api.fetchok.transport.test.ts index 84e480deaf8b..bd560545318b 100644 --- a/extensions/googlechat/src/api.fetchok.transport.test.ts +++ b/extensions/googlechat/src/api.fetchok.transport.test.ts @@ -64,6 +64,7 @@ vi.mock("./auth.js", () => ({ })); let deleteGoogleChatMessage: typeof import("./api.js").deleteGoogleChatMessage; +let sendGoogleChatMessage: typeof import("./api.js").sendGoogleChatMessage; const account = { accountId: "default", @@ -117,7 +118,7 @@ async function withinDeadline(promise: Promise, timeoutMs = 2_000): Promis describe("deleteGoogleChatMessage real guarded transport", () => { beforeAll(async () => { - ({ deleteGoogleChatMessage } = await import("./api.js")); + ({ deleteGoogleChatMessage, sendGoogleChatMessage } = await import("./api.js")); }); beforeEach(() => { @@ -133,6 +134,32 @@ describe("deleteGoogleChatMessage real guarded transport", () => { vi.restoreAllMocks(); }); + it("rejects malformed UTF-8 JSON through the real guarded transport", async () => { + const body = new Uint8Array([ + ...new TextEncoder().encode('{"name":"spaces/'), + 0xff, + ...new TextEncoder().encode('AAA"}'), + ]); + const server = createServer((_request, response) => { + response.writeHead(200, { "Content-Type": "application/json" }); + response.end(body); + }); + + loopback.baseUrl = await listen(server); + try { + const outcome = await withinDeadline( + sendGoogleChatMessage({ account, space: "spaces/AAA", text: "hello" }).then( + () => undefined, + (error: unknown) => error, + ), + ); + expect(outcome).toBeInstanceOf(Error); + expect((outcome as Error).message).toMatch(/malformed JSON response/); + } finally { + await closeServer(server); + } + }); + it("cancels a streaming authenticated DELETE before releasing its real dispatcher", async () => { let socketClosed = false; let receivedAuthorization: string | undefined; diff --git a/extensions/googlechat/src/api.ts b/extensions/googlechat/src/api.ts index 359cf803121c..ae85c4375477 100644 --- a/extensions/googlechat/src/api.ts +++ b/extensions/googlechat/src/api.ts @@ -6,6 +6,7 @@ import { parseMediaContentLength, readResponseTextSnippet, } from "openclaw/plugin-sdk/media-runtime"; +import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; @@ -33,17 +34,12 @@ function resolveGoogleChatMediaTimeoutMs(maxBytes?: number): number { } async function readGoogleChatJsonResponse(response: Response, label: string): Promise { - const bytes = await readResponseWithLimit(response, GOOGLECHAT_JSON_RESPONSE_MAX_BYTES, { + return readProviderJsonResponse(response, label, { + maxBytes: GOOGLECHAT_JSON_RESPONSE_MAX_BYTES, chunkTimeoutMs: GOOGLECHAT_RESPONSE_READ_IDLE_TIMEOUT_MS, onIdleTimeout: ({ chunkTimeoutMs }) => new Error(`${label}: response body stalled after ${chunkTimeoutMs}ms`), - onOverflow: ({ maxBytes }) => new Error(`${label}: JSON response exceeds ${maxBytes} bytes`), }); - try { - return JSON.parse(new TextDecoder().decode(bytes)) as T; - } catch (cause) { - throw new Error(`${label}: malformed JSON response`, { cause }); - } } async function readGoogleChatErrorResponse(response: Response, label: string): Promise { diff --git a/extensions/googlechat/src/targets.test.ts b/extensions/googlechat/src/targets.test.ts index a315bf0cba0c..5637183091a3 100644 --- a/extensions/googlechat/src/targets.test.ts +++ b/extensions/googlechat/src/targets.test.ts @@ -287,6 +287,43 @@ describe("googlechat group policy", () => { }); }); +describe("googlechat API JSON response decoding", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("rejects invalid UTF-8 in API JSON responses instead of corrupting identifiers", async () => { + const raw = Buffer.concat([ + Buffer.from('{"name":"spaces/'), + Buffer.from([0xff]), + Buffer.from('AAA"}'), + ]); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(new Uint8Array(raw), { status: 200 })), + ); + + await expect( + sendGoogleChatMessage({ account, space: "spaces/AAA", text: "hello" }), + ).rejects.toThrow(/malformed JSON response/); + }); + + it("keeps valid UTF-8 API JSON responses unchanged (negative control)", async () => { + vi.stubGlobal( + "fetch", + vi + .fn() + .mockResolvedValue( + new Response(new Uint8Array(Buffer.from('{"name":"spaces/AAA"}')), { status: 200 }), + ), + ); + + await expect( + sendGoogleChatMessage({ account, space: "spaces/AAA", text: "hello" }), + ).resolves.toEqual({ messageName: "spaces/AAA", threadName: undefined }); + }); +}); + describe("downloadGoogleChatMedia", () => { afterEach(() => { unregisterGoogleChatManualApprovalFollowupSuppression("12345678-1234-1234-1234-123456789012");