(
url: RequestInfo,
options: FetchOptions = {},
timeout: number,
maxRetries?: number,
)
| 624 | } |
| 625 | |
| 626 | export async function fetchWithRetries( |
| 627 | url: RequestInfo, |
| 628 | options: FetchOptions = {}, |
| 629 | timeout: number, |
| 630 | maxRetries?: number, |
| 631 | ): Promise<Response> { |
| 632 | const contextMaxRetries = getFetchRetryContextMaxRetries(); |
| 633 | maxRetries = Math.max(0, maxRetries ?? contextMaxRetries ?? 4); |
| 634 | |
| 635 | let lastErrorMessage: string | undefined; |
| 636 | const backoff = getEnvInt('PROMPTFOO_REQUEST_BACKOFF_MS', 5000); |
| 637 | |
| 638 | for (let i = 0; i <= maxRetries; i++) { |
| 639 | let response; |
| 640 | try { |
| 641 | // Disable transient retries in fetchWithProxy to avoid double-retrying |
| 642 | response = await fetchWithTimeout( |
| 643 | url, |
| 644 | { ...options, disableTransientRetries: true }, |
| 645 | timeout, |
| 646 | ); |
| 647 | |
| 648 | if (getEnvBool('PROMPTFOO_RETRY_5XX') && response.status >= 500 && response.status < 600) { |
| 649 | throw new Error(`Internal Server Error: ${response.status} ${response.statusText}`); |
| 650 | } |
| 651 | |
| 652 | if (response && isRateLimited(response)) { |
| 653 | await handleRateLimitedResponse(response, url, i, maxRetries); |
| 654 | continue; |
| 655 | } |
| 656 | |
| 657 | return response; |
| 658 | } catch (error) { |
| 659 | // Don't retry on abort - propagate immediately |
| 660 | if (error instanceof Error && error.name === 'AbortError') { |
| 661 | throw error; |
| 662 | } |
| 663 | |
| 664 | // Structured rate-limit errors are already final (quota fail-fast or |
| 665 | // retries exhausted) and carry retry-after / reset metadata. Don't |
| 666 | // swallow them in the generic retry path. |
| 667 | if (error instanceof HttpRateLimitError) { |
| 668 | throw error; |
| 669 | } |
| 670 | |
| 671 | const errorMessage = formatFetchErrorMessage(error); |
| 672 | |
| 673 | logger.debug( |
| 674 | `Request to ${urlForLog(url)} failed (attempt #${i + 1}), retrying: ${errorMessage}`, |
| 675 | ); |
| 676 | if (i < maxRetries) { |
| 677 | const waitTime = Math.pow(2, i) * (backoff + 1000 * Math.random()); |
| 678 | await sleep(waitTime); |
| 679 | } |
| 680 | lastErrorMessage = errorMessage; |
| 681 | } |
| 682 | } |
| 683 | throw new Error(`Request failed after ${maxRetries} retries: ${lastErrorMessage}`); |
no test coverage detected
searching dependent graphs…