Skip to content

Commit a7bfc06

Browse files
fix(google-media): bound JSON response reads (#96920)
* fix(google-media): bound JSON response reads * test(google): relax media response cap assertion --------- Co-authored-by: Vincent Koc <[email protected]>
1 parent c5d34c8 commit a7bfc06

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

extensions/google/media-understanding-provider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import {
1212
assertOkOrThrowProviderError,
1313
postJsonRequest,
14+
readProviderJsonResponse,
1415
type ProviderRequestTransportOverrides,
1516
} from "openclaw/plugin-sdk/provider-http";
1617
import {
@@ -97,11 +98,11 @@ async function generateGeminiInlineDataText(params: {
9798
try {
9899
await assertOkOrThrowProviderError(res, params.httpErrorLabel);
99100

100-
const payload = (await res.json()) as {
101+
const payload = await readProviderJsonResponse<{
101102
candidates?: Array<{
102103
content?: { parts?: Array<{ text?: string }> };
103104
}>;
104-
};
105+
}>(res, params.httpErrorLabel);
105106
const parts = payload.candidates?.[0]?.content?.parts ?? [];
106107
const text = parts
107108
.map((part) => part?.text?.trim())

extensions/google/media-understanding-provider.video.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Google tests cover media understanding provider.video plugin behavior.
2+
import { createServer, type Server } from "node:http";
23
import {
34
createRequestCaptureJsonFetch,
45
installPinnedHostnameTestHooks,
@@ -10,6 +11,49 @@ import { resolveGoogleGenerativeAiHttpRequestConfig } from "./runtime-api.js";
1011

1112
installPinnedHostnameTestHooks();
1213

14+
const LOOPBACK_RESPONSE_BYTES = 18 * 1024 * 1024;
15+
16+
async function listenLoopbackServer(server: Server): Promise<number> {
17+
return await new Promise((resolve, reject) => {
18+
server.once("error", reject);
19+
server.listen(0, "127.0.0.1", () => {
20+
server.off("error", reject);
21+
const address = server.address();
22+
if (!address || typeof address === "string") {
23+
reject(new Error("expected loopback TCP address"));
24+
return;
25+
}
26+
resolve(address.port);
27+
});
28+
});
29+
}
30+
31+
function createOversizedJsonServer(): { server: Server; closed: Promise<number> } {
32+
let resolveClosed: (sentBytes: number) => void = () => {};
33+
const closed = new Promise<number>((resolve) => {
34+
resolveClosed = resolve;
35+
});
36+
const server = createServer((_req, res) => {
37+
let sentBytes = 0;
38+
const chunk = Buffer.alloc(64 * 1024, 0x20);
39+
res.writeHead(200, { "content-type": "application/json" });
40+
const timer = setInterval(() => {
41+
if (sentBytes >= LOOPBACK_RESPONSE_BYTES) {
42+
clearInterval(timer);
43+
res.end();
44+
return;
45+
}
46+
sentBytes += chunk.length;
47+
res.write(chunk);
48+
}, 1);
49+
res.on("close", () => {
50+
clearInterval(timer);
51+
resolveClosed(sentBytes);
52+
});
53+
});
54+
return { server, closed };
55+
}
56+
1357
describe("describeGeminiVideo", () => {
1458
it("respects case-insensitive x-goog-api-key overrides", async () => {
1559
let seenKey: string | null = null;
@@ -114,6 +158,29 @@ describe("describeGeminiVideo", () => {
114158
);
115159
});
116160

161+
it("bounds oversized video JSON responses and closes the stream early", async () => {
162+
const { server, closed } = createOversizedJsonServer();
163+
const port = await listenLoopbackServer(server);
164+
const fetchFn = withFetchPreconnect(async () =>
165+
fetch(`http://127.0.0.1:${port}/google-video-json`),
166+
);
167+
168+
try {
169+
await expect(
170+
describeGeminiVideo({
171+
buffer: Buffer.from("video-bytes"),
172+
fileName: "clip.mp4",
173+
apiKey: "test-key",
174+
timeoutMs: 1500,
175+
fetchFn,
176+
}),
177+
).rejects.toThrow(/JSON response exceeds 16777216 bytes/u);
178+
await expect(closed).resolves.toBeLessThan(LOOPBACK_RESPONSE_BYTES);
179+
} finally {
180+
server.close();
181+
}
182+
});
183+
117184
it("rejects non-Google video base URLs before sending authenticated requests", async () => {
118185
await expect(
119186
describeGeminiVideo({

0 commit comments

Comments
 (0)