From 89c792bf646a489bb1a2ca8d2e781c14d97a262f Mon Sep 17 00:00:00 2001 From: sunlit-deng Date: Sun, 9 Aug 2026 09:15:39 +0800 Subject: [PATCH] fix(google): reject malformed video operation JSON (#115465) * fix(google): reject malformed video operation JSON * fix(google): reuse shared provider JSON reader for video operations Route successful Google video operation responses through the canonical readProviderJsonResponse so fatal UTF-8 decoding and malformed-JSON wrapping stay on the shared provider contract. Keep the lenient non-OK detail path. --- .../google/video-generation-provider.test.ts | 47 ++++++++++++++++++- .../google/video-generation-provider.ts | 21 ++++----- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/extensions/google/video-generation-provider.test.ts b/extensions/google/video-generation-provider.test.ts index dc3757333131..54ae86b9cdee 100644 --- a/extensions/google/video-generation-provider.test.ts +++ b/extensions/google/video-generation-provider.test.ts @@ -705,13 +705,58 @@ describe("google video generation provider", () => { cfg: {}, durationSeconds: 3, }), - ).rejects.toThrow("Google video operation response exceeds 16777216 bytes"); + ).rejects.toThrow("Google video operation response: JSON response exceeds 16777216 bytes"); expect(fetchMock).toHaveBeenCalledTimes(1); expect(streamed.getReadCount()).toBeLessThan(64); expect(streamed.wasCanceled()).toBe(true); }); + it("reports malformed Google REST operation JSON with a stable provider error", async () => { + vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ + apiKey: "google-key", + source: "env", + mode: "api-key", + }); + generateVideosMock.mockRejectedValue(Object.assign(new Error("sdk 404"), { status: 404 })); + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("{ nope", { status: 200 }))); + + await expect( + buildGoogleVideoGenerationProvider().generateVideo({ + provider: "google", + model: "veo-3.1-fast-generate-preview", + prompt: "A tiny robot watering a windowsill garden", + cfg: {}, + durationSeconds: 3, + }), + ).rejects.toThrow("Google video operation response: malformed JSON response"); + }); + + it("rejects invalid UTF-8 in Google REST operation JSON before parsing", async () => { + vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ + apiKey: "google-key", + source: "env", + mode: "api-key", + }); + generateVideosMock.mockRejectedValue(Object.assign(new Error("sdk 404"), { status: 404 })); + const invalidUtf8Json = new Uint8Array([ + ...Buffer.from('{"done":true,"name":"operations/'), + 0xff, + ...Buffer.from('"}'), + ]); + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(invalidUtf8Json))); + + await expect( + buildGoogleVideoGenerationProvider().generateVideo({ + provider: "google", + model: "veo-3.1-fast-generate-preview", + prompt: "A tiny robot watering a windowsill garden", + cfg: {}, + durationSeconds: 3, + }), + ).rejects.toThrow("Google video operation response: malformed JSON response"); + }); + it("retries transient Google REST poll failures with empty bodies", async () => { vi.useFakeTimers(); vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ diff --git a/extensions/google/video-generation-provider.ts b/extensions/google/video-generation-provider.ts index e02e3274462f..25e91ed1c4fc 100644 --- a/extensions/google/video-generation-provider.ts +++ b/extensions/google/video-generation-provider.ts @@ -4,6 +4,7 @@ import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runt import { createProviderOperationDeadline, executeProviderOperationWithRetry, + readProviderJsonResponse, resolveProviderOperationTimeoutMs, waitProviderOperationPollInterval, } from "openclaw/plugin-sdk/provider-http"; @@ -347,16 +348,13 @@ async function requestGoogleVideoJson(params: { signal: controller.signal, }); try { - const buffer = await readResponseWithLimit( - response, - GOOGLE_VIDEO_OPERATION_RESPONSE_MAX_BYTES, - { - onOverflow: ({ maxBytes }) => - new Error(`Google video operation response exceeds ${maxBytes} bytes`), - }, - ); - const text = new TextDecoder().decode(buffer); if (!response.ok) { + const text = new TextDecoder().decode( + await readResponseWithLimit(response, GOOGLE_VIDEO_OPERATION_RESPONSE_MAX_BYTES, { + onOverflow: ({ maxBytes }) => + new Error(`Google video operation response exceeds ${maxBytes} bytes`), + }), + ); let detail: unknown = text; if (text) { try { @@ -367,8 +365,9 @@ async function requestGoogleVideoJson(params: { } throw createHttpError(response, detail); } - const payload = text ? (JSON.parse(text) as unknown) : {}; - return payload; + return await readProviderJsonResponse(response, "Google video operation response", { + maxBytes: GOOGLE_VIDEO_OPERATION_RESPONSE_MAX_BYTES, + }); } finally { await release(); }