MCPcopy
hub / github.com/promptfoo/promptfoo / fetchAndReadBody

Function fetchAndReadBody

src/cache.ts:597–648  ·  view source on GitHub ↗
(
  url: RequestInfo,
  options: RequestInit,
  timeout: number,
  maxRetries: number | undefined,
  isIdempotent: boolean,
)

Source from the content-addressed store, hash-verified

595}
596
597async function fetchAndReadBody(
598 url: RequestInfo,
599 options: RequestInit,
600 timeout: number,
601 maxRetries: number | undefined,
602 isIdempotent: boolean,
603): Promise<{ respText: string; resp: Response; fetchLatencyMs: number }> {
604 const maxBodyRetries = isIdempotent ? 2 : 0;
605 for (let bodyAttempt = 0; bodyAttempt <= maxBodyRetries; bodyAttempt++) {
606 const fetchStart = Date.now();
607 // fetchWithRetries errors propagate directly — not caught by body retry
608 const resp = await fetchWithRetries(url, options, timeout, maxRetries);
609 const fetchLatencyMs = Date.now() - fetchStart;
610
611 try {
612 const respText = await resp.text();
613 return { respText, resp, fetchLatencyMs };
614 } catch (err) {
615 if (isTransientConnectionError(err as Error) && bodyAttempt < maxBodyRetries) {
616 const backoffMs = Math.pow(2, bodyAttempt) * 1000;
617 logger.debug('[Cache] Body stream failed with transient error, retrying', {
618 attempt: bodyAttempt + 1,
619 maxRetries: maxBodyRetries,
620 backoffMs,
621 error: (err as Error)?.message?.slice(0, 200),
622 });
623 await sleep(backoffMs);
624 continue;
625 }
626 // Preserve cancellation: an aborted body read rejects with an AbortError, and
627 // callers (e.g. evaluator.ts) suppress expected cancellation by checking
628 // `err.name === 'AbortError'`. Wrapping it would reset the name to 'Error' and
629 // turn cancelled evals into ordinary provider failures, so rethrow aborts as-is.
630 if (isAbortError(err)) {
631 throw err;
632 }
633 // Surface the URL and HTTP response context so opaque body-read failures
634 // (e.g. "TypeError: terminated" from a Cloudflare-originated 403) include
635 // actionable diagnostics instead of a bare platform error. Sanitize the URL so
636 // credential-bearing userinfo / query params are not leaked into logs.
637 const wrappedError = new Error(
638 `Error reading response body from ${sanitizeUrl(getRequestUrlString(url))}: ${
639 (err as Error).message
640 }. HTTP ${resp.status} ${resp.statusText}`,
641 ) as Error & { cause?: unknown };
642 wrappedError.cause = err;
643 throw wrappedError;
644 }
645 }
646 // Unreachable: loop always returns or throws, but TypeScript needs this
647 throw new Error('Exhausted body retries without returning or throwing');
648}
649
650async function prepareFetchResponse(
651 url: RequestInfo,

Callers 2

prepareFetchResponseFunction · 0.85
fetchWithCacheFunction · 0.85

Calls 6

fetchWithRetriesFunction · 0.90
sleepFunction · 0.90
isAbortErrorFunction · 0.90
sanitizeUrlFunction · 0.90
getRequestUrlStringFunction · 0.90

Tested by

no test coverage detected

Used in the wild real call sites across dependent graphs

searching dependent graphs…