@@ -46,6 +46,7 @@ type WorkflowRunSummary = {
|
46 | 46 | const DEFAULT_REPO = "openclaw/openclaw"; |
47 | 47 | const DEFAULT_CLAWHUB_REGISTRY = "https://clawhub.ai"; |
48 | 48 | const CLAWHUB_REQUEST_TIMEOUT_MS = 20_000; |
| 49 | +const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 1024 * 1024; |
49 | 50 | |
50 | 51 | function isRecord(value: unknown): value is JsonRecord { |
51 | 52 | return typeof value === "object" && value !== null && !Array.isArray(value); |
@@ -234,7 +235,63 @@ async function fetchJsonWithRetry(url: string): Promise<unknown> {
|
234 | 235 | if (!response.ok) { |
235 | 236 | throw new Error(`${url} returned HTTP ${response.status}.`); |
236 | 237 | } |
237 | | -return response.json() as Promise<unknown>; |
| 238 | +return await readBoundedJsonResponse(response, url); |
| 239 | +} |
| 240 | + |
| 241 | +async function readBoundedResponseText( |
| 242 | +response: Response, |
| 243 | +label: string, |
| 244 | +maxBytes = CLAWHUB_RESPONSE_BODY_MAX_BYTES, |
| 245 | +): Promise<string> { |
| 246 | +const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10); |
| 247 | +if (Number.isFinite(contentLength) && contentLength > maxBytes) { |
| 248 | +throw new Error(`${label} response body exceeded ${maxBytes} bytes.`); |
| 249 | +} |
| 250 | + |
| 251 | +if (!response.body) { |
| 252 | +return ""; |
| 253 | +} |
| 254 | + |
| 255 | +const reader = response.body.getReader(); |
| 256 | +const decoder = new TextDecoder(); |
| 257 | +const chunks: string[] = []; |
| 258 | +let totalBytes = 0; |
| 259 | +let canceled = false; |
| 260 | + |
| 261 | +try { |
| 262 | +for (;;) { |
| 263 | +const { done, value } = await reader.read(); |
| 264 | +if (done) { |
| 265 | +const tail = decoder.decode(); |
| 266 | +if (tail) { |
| 267 | +chunks.push(tail); |
| 268 | +} |
| 269 | +break; |
| 270 | +} |
| 271 | + |
| 272 | +totalBytes += value.byteLength; |
| 273 | +if (totalBytes > maxBytes) { |
| 274 | +canceled = true; |
| 275 | +await reader.cancel().catch(() => undefined); |
| 276 | +throw new Error(`${label} response body exceeded ${maxBytes} bytes.`); |
| 277 | +} |
| 278 | +chunks.push(decoder.decode(value, { stream: true })); |
| 279 | +} |
| 280 | +} finally { |
| 281 | +if (!canceled) { |
| 282 | +reader.releaseLock(); |
| 283 | +} |
| 284 | +} |
| 285 | + |
| 286 | +return chunks.join(""); |
| 287 | +} |
| 288 | + |
| 289 | +export async function readBoundedJsonResponse( |
| 290 | +response: Response, |
| 291 | +label: string, |
| 292 | +maxBytes = CLAWHUB_RESPONSE_BODY_MAX_BYTES, |
| 293 | +): Promise<unknown> { |
| 294 | +return parseJson(await readBoundedResponseText(response, label, maxBytes), label); |
238 | 295 | } |
239 | 296 | |
240 | 297 | async function fetchStatusWithRetry(url: string, method: "GET" | "HEAD"): Promise<number> { |
|