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.
This commit is contained in:
sunlit-deng
2026-08-09 09:15:39 +08:00
committed by GitHub
parent 224d8a9428
commit 89c792bf64
2 changed files with 56 additions and 12 deletions
@@ -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({
+10 -11
View File
@@ -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();
}