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");