Skip to content

Commit e9f508f

Browse files
committed
fix(google): bound video success response
1 parent 3ab8d6a commit e9f508f

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

extensions/google/video-generation-provider.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,39 @@ function fetchInputUrl(fetchMock: ReturnType<typeof vi.fn>, index: number): stri
9494
return input.url;
9595
}
9696

97+
function oversizedJsonResponse(params: { chunkCount: number; chunkSize: number }): {
98+
response: Response;
99+
getReadCount: () => number;
100+
wasCanceled: () => boolean;
101+
} {
102+
const chunk = new Uint8Array(params.chunkSize);
103+
let readCount = 0;
104+
let canceled = false;
105+
return {
106+
response: new Response(
107+
new ReadableStream<Uint8Array>({
108+
pull(controller) {
109+
if (readCount >= params.chunkCount) {
110+
controller.close();
111+
return;
112+
}
113+
readCount += 1;
114+
controller.enqueue(chunk);
115+
},
116+
cancel() {
117+
canceled = true;
118+
},
119+
}),
120+
{
121+
status: 200,
122+
headers: { "Content-Type": "application/json" },
123+
},
124+
),
125+
getReadCount: () => readCount,
126+
wasCanceled: () => canceled,
127+
};
128+
}
129+
97130
let ssrfMock: { mockRestore: () => void } | undefined;
98131

99132
describe("google video generation provider", () => {
@@ -486,6 +519,33 @@ describe("google video generation provider", () => {
486519
expect(result.videos[0]?.buffer).toEqual(Buffer.from("rest-video"));
487520
});
488521

522+
it("bounds successful Google REST operation JSON bodies instead of buffering the whole response", async () => {
523+
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
524+
apiKey: "google-key",
525+
source: "env",
526+
mode: "api-key",
527+
});
528+
generateVideosMock.mockRejectedValue(Object.assign(new Error("sdk 404"), { status: 404 }));
529+
const streamed = oversizedJsonResponse({ chunkCount: 64, chunkSize: 1024 * 1024 });
530+
const fetchMock = vi.fn(async () => streamed.response);
531+
vi.stubGlobal("fetch", fetchMock);
532+
533+
const provider = buildGoogleVideoGenerationProvider();
534+
await expect(
535+
provider.generateVideo({
536+
provider: "google",
537+
model: "veo-3.1-fast-generate-preview",
538+
prompt: "A tiny robot watering a windowsill garden",
539+
cfg: {},
540+
durationSeconds: 3,
541+
}),
542+
).rejects.toThrow("Google video operation response exceeds 16777216 bytes");
543+
544+
expect(fetchMock).toHaveBeenCalledTimes(1);
545+
expect(streamed.getReadCount()).toBeLessThan(64);
546+
expect(streamed.wasCanceled()).toBe(true);
547+
});
548+
489549
it("retries transient Google REST poll failures with empty bodies", async () => {
490550
vi.useFakeTimers();
491551
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({

extensions/google/video-generation-provider.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const DEFAULT_TIMEOUT_MS = 180_000;
2828
const POLL_INTERVAL_MS = 10_000;
2929
const MAX_POLL_ATTEMPTS = 120;
3030
const DEFAULT_GENERATED_VIDEO_MAX_BYTES = 16 * 1024 * 1024;
31+
const GOOGLE_VIDEO_OPERATION_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
3132
const GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE =
3233
"Google video generation response missing generated videos";
3334

@@ -349,7 +350,15 @@ async function requestGoogleVideoJson(params: {
349350
signal: controller.signal,
350351
});
351352
try {
352-
const text = await response.text();
353+
const buffer = await readResponseWithLimit(
354+
response,
355+
GOOGLE_VIDEO_OPERATION_RESPONSE_MAX_BYTES,
356+
{
357+
onOverflow: ({ maxBytes }) =>
358+
new Error(`Google video operation response exceeds ${maxBytes} bytes`),
359+
},
360+
);
361+
const text = new TextDecoder().decode(buffer);
353362
if (!response.ok) {
354363
let detail: unknown = text;
355364
if (text) {

0 commit comments

Comments
 (0)