From c183cb29111078eee03a0beb09c8ef34784c4abe Mon Sep 17 00:00:00 2001 From: Alix-007 Date: Fri, 26 Jun 2026 01:40:07 +0800 Subject: [PATCH] fix(qwen): bound video success response (#96604) (cherry picked from commit cc124d2921b88d241a567f17368bd55049d84f83) --- .../qwen/media-understanding-provider.test.ts | 71 +++++++++++++++++++ .../qwen/media-understanding-provider.ts | 10 ++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/extensions/qwen/media-understanding-provider.test.ts b/extensions/qwen/media-understanding-provider.test.ts index 4bc4c5580792..acdee732f249 100644 --- a/extensions/qwen/media-understanding-provider.test.ts +++ b/extensions/qwen/media-understanding-provider.test.ts @@ -8,6 +8,39 @@ import { describeQwenVideo } from "./media-understanding-provider.js"; installPinnedHostnameTestHooks(); +function oversizedJsonResponse(params: { chunkCount: number; chunkSize: number }): { + response: Response; + getReadCount: () => number; + wasCanceled: () => boolean; +} { + const chunk = new Uint8Array(params.chunkSize); + let readCount = 0; + let canceled = false; + return { + response: new Response( + new ReadableStream({ + pull(controller) { + if (readCount >= params.chunkCount) { + controller.close(); + return; + } + readCount += 1; + controller.enqueue(chunk); + }, + cancel() { + canceled = true; + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + getReadCount: () => readCount, + wasCanceled: () => canceled, + }; +} + describe("describeQwenVideo", () => { it("builds the expected OpenAI-compatible video payload", async () => { const { fetchFn, getRequest } = createRequestCaptureJsonFetch({ @@ -74,4 +107,42 @@ describe("describeQwenVideo", () => { `data:video/mp4;base64,${Buffer.from("video-bytes").toString("base64")}`, ); }); + + it("bounds successful Qwen video JSON bodies instead of buffering the whole response", async () => { + const streamed = oversizedJsonResponse({ chunkCount: 64, chunkSize: 1024 * 1024 }); + + await expect( + describeQwenVideo({ + buffer: Buffer.from("video-bytes"), + fileName: "clip.mp4", + mime: "video/mp4", + apiKey: "test-key", + timeoutMs: 1500, + baseUrl: "https://example.com/v1", + fetchFn: async () => streamed.response, + }), + ).rejects.toThrow("Qwen video description failed: JSON response exceeds 16777216 bytes"); + + expect(streamed.getReadCount()).toBeLessThan(64); + expect(streamed.wasCanceled()).toBe(true); + }); + + it("reports malformed Qwen video JSON with a provider-owned error", async () => { + const response = new Response("not-json{", { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + + await expect( + describeQwenVideo({ + buffer: Buffer.from("video-bytes"), + fileName: "clip.mp4", + mime: "video/mp4", + apiKey: "test-key", + timeoutMs: 1500, + baseUrl: "https://example.com/v1", + fetchFn: async () => response, + }), + ).rejects.toThrow("Qwen video description failed: malformed JSON response"); + }); }); diff --git a/extensions/qwen/media-understanding-provider.ts b/extensions/qwen/media-understanding-provider.ts index 5ecf5729ba9d..b1e22a9174ec 100644 --- a/extensions/qwen/media-understanding-provider.ts +++ b/extensions/qwen/media-understanding-provider.ts @@ -13,6 +13,7 @@ import { import { assertOkOrThrowHttpError, postJsonRequest, + readProviderJsonResponse, resolveProviderHttpRequestConfig, } from "openclaw/plugin-sdk/provider-http"; import { QWEN_STANDARD_GLOBAL_BASE_URL } from "./models.js"; @@ -60,7 +61,14 @@ export async function describeQwenVideo( try { await assertOkOrThrowHttpError(res, "Qwen video description failed"); - const payload = (await res.json()) as OpenAiCompatibleVideoPayload; + // Read the success body through the shared byte-bounded JSON reader (16 MiB cap + + // stream cancel on overflow) so a hostile or buggy endpoint cannot force the runtime + // to buffer an unbounded body. Malformed JSON keeps the + // `Qwen video description failed: malformed JSON response` wrapping. + const payload = await readProviderJsonResponse( + res, + "Qwen video description failed", + ); const text = coerceOpenAiCompatibleVideoText(payload); if (!text) { throw new Error("Qwen video description response missing content");