fix: improve retry logic for fetch errors and network codes (#14439)

This commit is contained in:
Megha Bansal
2025-12-04 21:11:08 +05:30
committed by GitHub
parent b745d46395
commit 6f3b56c5b6
2 changed files with 89 additions and 14 deletions

View File

@@ -307,11 +307,50 @@ describe('retryWithBackoff', () => {
});
describe('Fetch error retries', () => {
const fetchErrorMsg = 'exception TypeError: fetch failed sending request';
it('should retry on specific fetch error when retryFetchErrors is true', async () => {
const mockFn = vi.fn();
mockFn.mockRejectedValueOnce(new Error(fetchErrorMsg));
mockFn.mockRejectedValueOnce(new TypeError('fetch failed'));
mockFn.mockResolvedValueOnce('success');
const promise = retryWithBackoff(mockFn, {
retryFetchErrors: true,
initialDelayMs: 10,
});
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe('success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should retry on common network error codes (ECONNRESET)', async () => {
const mockFn = vi.fn();
const error = new Error('read ECONNRESET');
(error as any).code = 'ECONNRESET';
mockFn.mockRejectedValueOnce(error);
mockFn.mockResolvedValueOnce('success');
const promise = retryWithBackoff(mockFn, {
retryFetchErrors: true,
initialDelayMs: 10,
});
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe('success');
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('should retry on common network error codes in cause (ETIMEDOUT)', async () => {
const mockFn = vi.fn();
const cause = new Error('Connect Timeout');
(cause as any).code = 'ETIMEDOUT';
const error = new Error('fetch failed');
(error as any).cause = cause;
mockFn.mockRejectedValueOnce(error);
mockFn.mockResolvedValueOnce('success');
const promise = retryWithBackoff(mockFn, {
@@ -329,13 +368,13 @@ describe('retryWithBackoff', () => {
it.each([false, undefined])(
'should not retry on specific fetch error when retryFetchErrors is %s',
async (retryFetchErrors) => {
const mockFn = vi.fn().mockRejectedValue(new Error(fetchErrorMsg));
const mockFn = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
const promise = retryWithBackoff(mockFn, {
retryFetchErrors,
});
await expect(promise).rejects.toThrow(fetchErrorMsg);
await expect(promise).rejects.toThrow('fetch failed');
expect(mockFn).toHaveBeenCalledTimes(1);
},
);

View File

@@ -16,9 +16,6 @@ import { delay, createAbortError } from './delay.js';
import { debugLogger } from './debugLogger.js';
import { getErrorStatus, ModelNotFoundError } from './httpErrors.js';
const FETCH_FAILED_MESSAGE =
'exception TypeError: fetch failed sending request';
export interface RetryOptions {
maxAttempts: number;
initialDelayMs: number;
@@ -41,6 +38,40 @@ const DEFAULT_RETRY_OPTIONS: RetryOptions = {
shouldRetryOnError: defaultShouldRetry,
};
const RETRYABLE_NETWORK_CODES = [
'ECONNRESET',
'ETIMEDOUT',
'EPIPE',
'ENOTFOUND',
'EAI_AGAIN',
'ECONNREFUSED',
];
function getNetworkErrorCode(error: unknown): string | undefined {
const getCode = (obj: unknown): string | undefined => {
if (typeof obj !== 'object' || obj === null) {
return undefined;
}
if ('code' in obj && typeof (obj as { code: unknown }).code === 'string') {
return (obj as { code: string }).code;
}
return undefined;
};
const directCode = getCode(error);
if (directCode) {
return directCode;
}
if (typeof error === 'object' && error !== null && 'cause' in error) {
return getCode((error as { cause: unknown }).cause);
}
return undefined;
}
const FETCH_FAILED_MESSAGE = 'fetch failed';
/**
* Default predicate function to determine if a retry should be attempted.
* Retries on 429 (Too Many Requests) and 5xx server errors.
@@ -52,12 +83,17 @@ function defaultShouldRetry(
error: Error | unknown,
retryFetchErrors?: boolean,
): boolean {
if (
retryFetchErrors &&
error instanceof Error &&
error.message.includes(FETCH_FAILED_MESSAGE)
) {
return true;
if (retryFetchErrors && error instanceof Error) {
// Check for generic fetch failed message (case-insensitive)
if (error.message.toLowerCase().includes(FETCH_FAILED_MESSAGE)) {
return true;
}
// Check for common network error codes
const errorCode = getNetworkErrorCode(error);
if (errorCode && RETRYABLE_NETWORK_CODES.includes(errorCode)) {
return true;
}
}
// Priority check for ApiError