@@ -25,6 +25,7 @@ const ISSUE_MEMORY_FILE_COUNT = ISSUE_FILE_COUNTS.reduce((sum, [, count]) => sum
|
25 | 25 | const DEFAULT_FILE_COUNT = 512; |
26 | 26 | const DEFAULT_MAX_WORKSPACE_REG_FDS = process.platform === "darwin" ? 8 : 64; |
27 | 27 | export const GATEWAY_READY_OUTPUT_MAX_CHARS = 128 * 1024; |
| 28 | +export const MEMORY_SEARCH_RESPONSE_MAX_BYTES = 256 * 1024; |
28 | 29 | |
29 | 30 | const SKIP_GATEWAY_ENV = { |
30 | 31 | NODE_ENV: "test", |
@@ -431,6 +432,55 @@ export async function stopGatewayWithRuntime({
|
431 | 432 | } |
432 | 433 | } |
433 | 434 | |
| 435 | +function responseBodyTooLargeError(label, maxBytes) { |
| 436 | +return new Error(`${label} response body exceeded ${maxBytes} bytes`); |
| 437 | +} |
| 438 | + |
| 439 | +export async function readBoundedResponseText(response, label, maxBytes) { |
| 440 | +const contentLength = Number(response.headers.get("content-length") ?? ""); |
| 441 | +if (Number.isSafeInteger(contentLength) && contentLength > maxBytes) { |
| 442 | +await response.body?.cancel().catch(() => undefined); |
| 443 | +throw responseBodyTooLargeError(label, maxBytes); |
| 444 | +} |
| 445 | + |
| 446 | +if (!response.body) { |
| 447 | +return ""; |
| 448 | +} |
| 449 | + |
| 450 | +const reader = response.body.getReader(); |
| 451 | +const decoder = new TextDecoder(); |
| 452 | +const chunks = []; |
| 453 | +let totalBytes = 0; |
| 454 | +let canceled = false; |
| 455 | + |
| 456 | +try { |
| 457 | +for (;;) { |
| 458 | +const { done, value } = await reader.read(); |
| 459 | +if (done) { |
| 460 | +const tail = decoder.decode(); |
| 461 | +if (tail) { |
| 462 | +chunks.push(tail); |
| 463 | +} |
| 464 | +break; |
| 465 | +} |
| 466 | + |
| 467 | +totalBytes += value.byteLength; |
| 468 | +if (totalBytes > maxBytes) { |
| 469 | +canceled = true; |
| 470 | +await reader.cancel().catch(() => undefined); |
| 471 | +throw responseBodyTooLargeError(label, maxBytes); |
| 472 | +} |
| 473 | +chunks.push(decoder.decode(value, { stream: true })); |
| 474 | +} |
| 475 | +} finally { |
| 476 | +if (!canceled) { |
| 477 | +reader.releaseLock(); |
| 478 | +} |
| 479 | +} |
| 480 | + |
| 481 | +return chunks.join(""); |
| 482 | +} |
| 483 | + |
434 | 484 | async function invokeMemorySearch({ port, token, timeoutMs }) { |
435 | 485 | const controller = new AbortController(); |
436 | 486 | const timer = setTimeout(() => controller.abort(), timeoutMs); |
@@ -453,7 +503,11 @@ async function invokeMemorySearch({ port, token, timeoutMs }) {
|
453 | 503 | }), |
454 | 504 | signal: controller.signal, |
455 | 505 | }); |
456 | | -const text = await res.text(); |
| 506 | +const text = await readBoundedResponseText( |
| 507 | +res, |
| 508 | +"memory_search", |
| 509 | +MEMORY_SEARCH_RESPONSE_MAX_BYTES, |
| 510 | +); |
457 | 511 | return { |
458 | 512 | ok: res.ok, |
459 | 513 | status: res.status, |
|