Skip to content

Commit 88125a0

Browse files
committed
fix(vydra): bound control response reads
1 parent b7a9d30 commit 88125a0

6 files changed

Lines changed: 88 additions & 3 deletions

File tree

extensions/vydra/image-generation-provider.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ function fetchCall(fetchMock: ReturnType<typeof vi.fn>, index = 0): [string, Req
1717
return call as [string, RequestInit];
1818
}
1919

20+
function oversizedJsonResponse(): Response {
21+
return new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), {
22+
status: 200,
23+
headers: { "Content-Type": "application/json" },
24+
});
25+
}
26+
2027
describe("vydra image-generation provider", () => {
2128
installPinnedHostnameTestHooks();
2229

@@ -94,6 +101,21 @@ describe("vydra image-generation provider", () => {
94101
).rejects.toThrow("Vydra image download exceeds 1 bytes");
95102
});
96103

104+
it("rejects image creation JSON responses that exceed the provider cap", async () => {
105+
stubVydraApiKey();
106+
stubFetch(oversizedJsonResponse());
107+
108+
const provider = buildVydraImageGenerationProvider();
109+
await expect(
110+
provider.generateImage({
111+
provider: "vydra",
112+
model: "grok-imagine",
113+
prompt: "draw a cat",
114+
cfg: {},
115+
}),
116+
).rejects.toThrow("vydra.image-generation: JSON response exceeds 16777216 bytes");
117+
});
118+
97119
it("passes request SSRF policy to the image creation request", async () => {
98120
stubVydraApiKey();
99121
const fetchMock = stubFetch(
@@ -151,4 +173,19 @@ describe("vydra image-generation provider", () => {
151173
expect(pollCall[0]).toBe("https://www.vydra.ai/api/v1/jobs/job-456");
152174
expect(pollCall[1].method).toBe("GET");
153175
});
176+
177+
it("rejects job poll JSON responses that exceed the provider cap", async () => {
178+
stubVydraApiKey();
179+
stubFetch(jsonResponse({ jobId: "job-456", status: "queued" }), oversizedJsonResponse());
180+
181+
const provider = buildVydraImageGenerationProvider();
182+
await expect(
183+
provider.generateImage({
184+
provider: "vydra",
185+
model: "grok-imagine",
186+
prompt: "draw a cat",
187+
cfg: {},
188+
}),
189+
).rejects.toThrow("Vydra job status: JSON response exceeds 16777216 bytes");
190+
});
154191
});

extensions/vydra/shared.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
assertOkOrThrowHttpError,
77
createProviderOperationDeadline,
88
fetchWithTimeout,
9+
readProviderJsonResponse,
910
resolveProviderOperationTimeoutMs,
1011
resolveProviderHttpRequestConfig,
1112
waitProviderOperationPollInterval,
@@ -287,7 +288,7 @@ async function waitForVydraJob(params: {
287288
params.fetchFn,
288289
);
289290
await assertOkOrThrowHttpError(response, "Vydra job status request failed");
290-
const payload = await response.json();
291+
const payload = await readProviderJsonResponse<unknown>(response, "Vydra job status");
291292
const status = resolveVydraResponseStatus(payload);
292293
if (status === "completed" || extractVydraResultUrls(payload, params.kind).length > 0) {
293294
return payload;

extensions/vydra/speech-provider.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ describe("vydra speech provider", () => {
88

99
const provider = buildVydraSpeechProvider();
1010

11+
const oversizedJsonResponse = () =>
12+
new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), {
13+
status: 200,
14+
headers: { "Content-Type": "application/json" },
15+
});
16+
1117
afterEach(() => {
1218
vi.unstubAllGlobals();
1319
vi.restoreAllMocks();
@@ -103,4 +109,18 @@ describe("vydra speech provider", () => {
103109
}),
104110
).rejects.toThrow("Vydra audio download exceeds 1 bytes");
105111
});
112+
113+
it("rejects speech synthesis JSON responses that exceed the provider cap", async () => {
114+
vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce(oversizedJsonResponse()));
115+
116+
await expect(
117+
provider.synthesize({
118+
text: "OpenClaw test",
119+
cfg: {} as never,
120+
providerConfig: { apiKey: "vydra-test-key" },
121+
target: "audio-file",
122+
timeoutMs: 30_000,
123+
}),
124+
).rejects.toThrow("Vydra speech synthesis: JSON response exceeds 16777216 bytes");
125+
});
106126
});

extensions/vydra/speech-provider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {
33
assertOkOrThrowHttpError,
44
postJsonRequest,
5+
readProviderJsonResponse,
56
resolveProviderHttpRequestConfig,
67
} from "openclaw/plugin-sdk/provider-http";
78
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
@@ -129,7 +130,7 @@ export function buildVydraSpeechProvider(): SpeechProviderPlugin {
129130

130131
try {
131132
await assertOkOrThrowHttpError(response, "Vydra speech synthesis failed");
132-
const payload = await response.json();
133+
const payload = await readProviderJsonResponse<unknown>(response, "Vydra speech synthesis");
133134
const audioUrl = extractVydraResultUrls(payload, "audio")[0];
134135
if (!audioUrl) {
135136
throw new Error("Vydra speech synthesis response missing audio URL");

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ function fetchCall(fetchMock: ReturnType<typeof vi.fn>, index: number) {
1818
return call;
1919
}
2020

21+
function oversizedJsonResponse(): Response {
22+
return new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), {
23+
status: 200,
24+
headers: { "Content-Type": "application/json" },
25+
});
26+
}
27+
2128
describe("vydra video-generation provider", () => {
2229
installPinnedHostnameTestHooks();
2330

@@ -96,6 +103,21 @@ describe("vydra video-generation provider", () => {
96103
).rejects.toThrow("Vydra video download exceeds 1 bytes");
97104
});
98105

106+
it("rejects video creation JSON responses that exceed the provider cap", async () => {
107+
stubVydraApiKey();
108+
stubFetch(oversizedJsonResponse());
109+
110+
const provider = buildVydraVideoGenerationProvider();
111+
await expect(
112+
provider.generateVideo({
113+
provider: "vydra",
114+
model: "veo3",
115+
prompt: "tiny city at sunrise",
116+
cfg: {},
117+
}),
118+
).rejects.toThrow("Vydra video generation: JSON response exceeds 16777216 bytes");
119+
});
120+
99121
it("requires a remote image url for kling", async () => {
100122
stubVydraApiKey();
101123
vi.stubGlobal("fetch", vi.fn());

extensions/vydra/video-generation-provider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createProviderOperationDeadline,
66
createProviderOperationTimeoutResolver,
77
postJsonRequest,
8+
readProviderJsonResponse,
89
resolveProviderOperationTimeoutMs,
910
} from "openclaw/plugin-sdk/provider-http";
1011
import type { VideoGenerationProvider } from "openclaw/plugin-sdk/video-generation";
@@ -111,7 +112,10 @@ export function buildVydraVideoGenerationProvider(): VideoGenerationProvider {
111112

112113
try {
113114
await assertOkOrThrowHttpError(response, "Vydra video generation failed");
114-
const submitted = await response.json();
115+
const submitted = await readProviderJsonResponse<unknown>(
116+
response,
117+
"Vydra video generation",
118+
);
115119
const completedPayload = await resolveCompletedVydraPayload({
116120
submitted,
117121
baseUrl,

0 commit comments

Comments
 (0)