


















@@ -6,6 +6,7 @@ type FetchJsonParams = {
66fetchImpl?: (url: string, init: RequestInit) => Promise<Response>;
77init: RequestInit;
88label: string;
9+maxBodyBytes?: number;
910timeoutMs: number;
1011url: string;
1112};
@@ -17,12 +18,42 @@ type RunCommandOptions = {
1718};
18191920const DEFAULT_OUTPUT_LIMIT = 128 * 1024;
21+const DEFAULT_FETCH_BODY_LIMIT = 1024 * 1024;
2022const KILL_GRACE_MS = 5_000;
21232224function timeoutError(message: string) {
2325return Object.assign(new Error(message), { code: "ETIMEDOUT" });
2426}
252728+function bodyTooLargeError(message: string) {
29+return Object.assign(new Error(message), { code: "ETOOBIG" });
30+}
31+32+function resolveFetchBodyLimit(limit: number | undefined) {
33+if (limit !== undefined) {
34+if (!Number.isSafeInteger(limit) || limit < 1) {
35+throw new Error(`fetch JSON body limit must be a positive integer; got: ${limit}`);
36+}
37+return limit;
38+}
39+const raw = process.env.OPENCLAW_QA_CREDENTIAL_HTTP_MAX_BODY_BYTES?.trim();
40+if (!raw) {
41+return DEFAULT_FETCH_BODY_LIMIT;
42+}
43+if (!/^\d+$/u.test(raw)) {
44+throw new Error(
45+`OPENCLAW_QA_CREDENTIAL_HTTP_MAX_BODY_BYTES must be a positive integer; got: ${raw}`,
46+);
47+}
48+const parsed = Number(raw);
49+if (!Number.isSafeInteger(parsed) || parsed < 1) {
50+throw new Error(
51+`OPENCLAW_QA_CREDENTIAL_HTTP_MAX_BODY_BYTES must be a positive integer; got: ${raw}`,
52+);
53+}
54+return parsed;
55+}
56+2657function appendBounded(previous: string, chunk: Buffer, limit: number) {
2758const next = previous + chunk.toString();
2859if (next.length <= limit) {
@@ -107,8 +138,49 @@ export function runCommand(
107138});
108139}
109140141+async function readBoundedResponseText(
142+response: Response,
143+label: string,
144+byteLimit: number,
145+timeoutPromise: Promise<never>,
146+) {
147+const contentLength = response.headers.get("content-length");
148+if (contentLength) {
149+const parsedLength = Number(contentLength);
150+if (Number.isSafeInteger(parsedLength) && parsedLength > byteLimit) {
151+await response.body?.cancel().catch(() => {});
152+throw bodyTooLargeError(`${label} response body exceeded ${byteLimit} bytes`);
153+}
154+}
155+if (!response.body) {
156+return "";
157+}
158+159+const reader = response.body.getReader();
160+const decoder = new TextDecoder();
161+let byteCount = 0;
162+let text = "";
163+try {
164+while (true) {
165+const { done, value } = await Promise.race([reader.read(), timeoutPromise]);
166+if (done) {
167+return text + decoder.decode();
168+}
169+byteCount += value.byteLength;
170+if (byteCount > byteLimit) {
171+await reader.cancel().catch(() => {});
172+throw bodyTooLargeError(`${label} response body exceeded ${byteLimit} bytes`);
173+}
174+text += decoder.decode(value, { stream: true });
175+}
176+} finally {
177+reader.releaseLock();
178+}
179+}
180+110181export async function fetchJsonWithTimeout(params: FetchJsonParams) {
111182const timeoutMs = Math.max(1, params.timeoutMs);
183+const maxBodyBytes = resolveFetchBodyLimit(params.maxBodyBytes);
112184const controller = new AbortController();
113185const error = timeoutError(`${params.label} timed out after ${timeoutMs}ms`);
114186let timeout: NodeJS.Timeout | undefined;
@@ -128,7 +200,13 @@ export async function fetchJsonWithTimeout(params: FetchJsonParams) {
128200}),
129201timeoutPromise,
130202]);
131-const payload = (await Promise.race([response.json(), timeoutPromise])) as JsonObject;
203+const rawPayload = await readBoundedResponseText(
204+response,
205+params.label,
206+maxBodyBytes,
207+timeoutPromise,
208+);
209+const payload = JSON.parse(rawPayload) as JsonObject;
132210return { payload, response };
133211} finally {
134212if (timeout) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。