From e40f3ee89a09d93fe2ca40569229ea74bd27dae8 Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Mon, 29 Jun 2026 12:11:23 +0800
Subject: [PATCH] fix(vydra): bound control response reads
Reviewed and accepted after live preflight: mergeable clean, checks passing, no unresolved review threads.
(cherry picked from commit 74a9beb83f515c805b41d12bdeddf33b7128490f)
---
.../vydra/image-generation-provider.test.ts | 37 +++++++++++++++++++
extensions/vydra/shared.ts | 3 +-
extensions/vydra/speech-provider.test.ts | 20 ++++++++++
extensions/vydra/speech-provider.ts | 3 +-
.../vydra/video-generation-provider.test.ts | 22 +++++++++++
extensions/vydra/video-generation-provider.ts | 6 ++-
6 files changed, 88 insertions(+), 3 deletions(-)
diff --git a/extensions/vydra/image-generation-provider.test.ts b/extensions/vydra/image-generation-provider.test.ts
index e98a21cdf99a..514af26ab337 100644
--- a/extensions/vydra/image-generation-provider.test.ts
+++ b/extensions/vydra/image-generation-provider.test.ts
@@ -17,6 +17,13 @@ function fetchCall(fetchMock: ReturnType, index = 0): [string, Req
return call as [string, RequestInit];
}
+function oversizedJsonResponse(): Response {
+ return new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+}
+
describe("vydra image-generation provider", () => {
installPinnedHostnameTestHooks();
@@ -94,6 +101,21 @@ describe("vydra image-generation provider", () => {
).rejects.toThrow("Vydra image download exceeds 1 bytes");
});
+ it("rejects image creation JSON responses that exceed the provider cap", async () => {
+ stubVydraApiKey();
+ stubFetch(oversizedJsonResponse());
+
+ const provider = buildVydraImageGenerationProvider();
+ await expect(
+ provider.generateImage({
+ provider: "vydra",
+ model: "grok-imagine",
+ prompt: "draw a cat",
+ cfg: {},
+ }),
+ ).rejects.toThrow("vydra.image-generation: JSON response exceeds 16777216 bytes");
+ });
+
it("passes request SSRF policy to the image creation request", async () => {
stubVydraApiKey();
const fetchMock = stubFetch(
@@ -151,4 +173,19 @@ describe("vydra image-generation provider", () => {
expect(pollCall[0]).toBe("https://www.vydra.ai/api/v1/jobs/job-456");
expect(pollCall[1].method).toBe("GET");
});
+
+ it("rejects job poll JSON responses that exceed the provider cap", async () => {
+ stubVydraApiKey();
+ stubFetch(jsonResponse({ jobId: "job-456", status: "queued" }), oversizedJsonResponse());
+
+ const provider = buildVydraImageGenerationProvider();
+ await expect(
+ provider.generateImage({
+ provider: "vydra",
+ model: "grok-imagine",
+ prompt: "draw a cat",
+ cfg: {},
+ }),
+ ).rejects.toThrow("Vydra job status: JSON response exceeds 16777216 bytes");
+ });
});
diff --git a/extensions/vydra/shared.ts b/extensions/vydra/shared.ts
index 6532c9c073a4..1bec44e714b7 100644
--- a/extensions/vydra/shared.ts
+++ b/extensions/vydra/shared.ts
@@ -6,6 +6,7 @@ import {
assertOkOrThrowHttpError,
createProviderOperationDeadline,
fetchWithTimeout,
+ readProviderJsonResponse,
resolveProviderOperationTimeoutMs,
resolveProviderHttpRequestConfig,
waitProviderOperationPollInterval,
@@ -287,7 +288,7 @@ async function waitForVydraJob(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "Vydra job status request failed");
- const payload = await response.json();
+ const payload = await readProviderJsonResponse(response, "Vydra job status");
const status = resolveVydraResponseStatus(payload);
if (status === "completed" || extractVydraResultUrls(payload, params.kind).length > 0) {
return payload;
diff --git a/extensions/vydra/speech-provider.test.ts b/extensions/vydra/speech-provider.test.ts
index f669afc03890..c6a19bf0beed 100644
--- a/extensions/vydra/speech-provider.test.ts
+++ b/extensions/vydra/speech-provider.test.ts
@@ -8,6 +8,12 @@ describe("vydra speech provider", () => {
const provider = buildVydraSpeechProvider();
+ const oversizedJsonResponse = () =>
+ new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
@@ -103,4 +109,18 @@ describe("vydra speech provider", () => {
}),
).rejects.toThrow("Vydra audio download exceeds 1 bytes");
});
+
+ it("rejects speech synthesis JSON responses that exceed the provider cap", async () => {
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce(oversizedJsonResponse()));
+
+ await expect(
+ provider.synthesize({
+ text: "OpenClaw test",
+ cfg: {} as never,
+ providerConfig: { apiKey: "vydra-test-key" },
+ target: "audio-file",
+ timeoutMs: 30_000,
+ }),
+ ).rejects.toThrow("Vydra speech synthesis: JSON response exceeds 16777216 bytes");
+ });
});
diff --git a/extensions/vydra/speech-provider.ts b/extensions/vydra/speech-provider.ts
index 74e02e0c91a0..3a2b6daefece 100644
--- a/extensions/vydra/speech-provider.ts
+++ b/extensions/vydra/speech-provider.ts
@@ -2,6 +2,7 @@
import {
assertOkOrThrowHttpError,
postJsonRequest,
+ readProviderJsonResponse,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
@@ -129,7 +130,7 @@ export function buildVydraSpeechProvider(): SpeechProviderPlugin {
try {
await assertOkOrThrowHttpError(response, "Vydra speech synthesis failed");
- const payload = await response.json();
+ const payload = await readProviderJsonResponse(response, "Vydra speech synthesis");
const audioUrl = extractVydraResultUrls(payload, "audio")[0];
if (!audioUrl) {
throw new Error("Vydra speech synthesis response missing audio URL");
diff --git a/extensions/vydra/video-generation-provider.test.ts b/extensions/vydra/video-generation-provider.test.ts
index c748bee5ccda..c66f1156a6a2 100644
--- a/extensions/vydra/video-generation-provider.test.ts
+++ b/extensions/vydra/video-generation-provider.test.ts
@@ -18,6 +18,13 @@ function fetchCall(fetchMock: ReturnType, index: number) {
return call;
}
+function oversizedJsonResponse(): Response {
+ return new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+}
+
describe("vydra video-generation provider", () => {
installPinnedHostnameTestHooks();
@@ -96,6 +103,21 @@ describe("vydra video-generation provider", () => {
).rejects.toThrow("Vydra video download exceeds 1 bytes");
});
+ it("rejects video creation JSON responses that exceed the provider cap", async () => {
+ stubVydraApiKey();
+ stubFetch(oversizedJsonResponse());
+
+ const provider = buildVydraVideoGenerationProvider();
+ await expect(
+ provider.generateVideo({
+ provider: "vydra",
+ model: "veo3",
+ prompt: "tiny city at sunrise",
+ cfg: {},
+ }),
+ ).rejects.toThrow("Vydra video generation: JSON response exceeds 16777216 bytes");
+ });
+
it("requires a remote image url for kling", async () => {
stubVydraApiKey();
vi.stubGlobal("fetch", vi.fn());
diff --git a/extensions/vydra/video-generation-provider.ts b/extensions/vydra/video-generation-provider.ts
index 7e8e95816d54..02310b558b0a 100644
--- a/extensions/vydra/video-generation-provider.ts
+++ b/extensions/vydra/video-generation-provider.ts
@@ -5,6 +5,7 @@ import {
createProviderOperationDeadline,
createProviderOperationTimeoutResolver,
postJsonRequest,
+ readProviderJsonResponse,
resolveProviderOperationTimeoutMs,
} from "openclaw/plugin-sdk/provider-http";
import type { VideoGenerationProvider } from "openclaw/plugin-sdk/video-generation";
@@ -111,7 +112,10 @@ export function buildVydraVideoGenerationProvider(): VideoGenerationProvider {
try {
await assertOkOrThrowHttpError(response, "Vydra video generation failed");
- const submitted = await response.json();
+ const submitted = await readProviderJsonResponse(
+ response,
+ "Vydra video generation",
+ );
const completedPayload = await resolveCompletedVydraPayload({
submitted,
baseUrl,