mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
fix: improve retry logic for fetch errors and network codes (#14439)
This commit is contained in:
@@ -307,11 +307,50 @@ describe('retryWithBackoff', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Fetch error retries', () => {
|
describe('Fetch error retries', () => {
|
||||||
const fetchErrorMsg = 'exception TypeError: fetch failed sending request';
|
|
||||||
|
|
||||||
it('should retry on specific fetch error when retryFetchErrors is true', async () => {
|
it('should retry on specific fetch error when retryFetchErrors is true', async () => {
|
||||||
const mockFn = vi.fn();
|
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');
|
mockFn.mockResolvedValueOnce('success');
|
||||||
|
|
||||||
const promise = retryWithBackoff(mockFn, {
|
const promise = retryWithBackoff(mockFn, {
|
||||||
@@ -329,13 +368,13 @@ describe('retryWithBackoff', () => {
|
|||||||
it.each([false, undefined])(
|
it.each([false, undefined])(
|
||||||
'should not retry on specific fetch error when retryFetchErrors is %s',
|
'should not retry on specific fetch error when retryFetchErrors is %s',
|
||||||
async (retryFetchErrors) => {
|
async (retryFetchErrors) => {
|
||||||
const mockFn = vi.fn().mockRejectedValue(new Error(fetchErrorMsg));
|
const mockFn = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
|
||||||
|
|
||||||
const promise = retryWithBackoff(mockFn, {
|
const promise = retryWithBackoff(mockFn, {
|
||||||
retryFetchErrors,
|
retryFetchErrors,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(promise).rejects.toThrow(fetchErrorMsg);
|
await expect(promise).rejects.toThrow('fetch failed');
|
||||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ import { delay, createAbortError } from './delay.js';
|
|||||||
import { debugLogger } from './debugLogger.js';
|
import { debugLogger } from './debugLogger.js';
|
||||||
import { getErrorStatus, ModelNotFoundError } from './httpErrors.js';
|
import { getErrorStatus, ModelNotFoundError } from './httpErrors.js';
|
||||||
|
|
||||||
const FETCH_FAILED_MESSAGE =
|
|
||||||
'exception TypeError: fetch failed sending request';
|
|
||||||
|
|
||||||
export interface RetryOptions {
|
export interface RetryOptions {
|
||||||
maxAttempts: number;
|
maxAttempts: number;
|
||||||
initialDelayMs: number;
|
initialDelayMs: number;
|
||||||
@@ -41,6 +38,40 @@ const DEFAULT_RETRY_OPTIONS: RetryOptions = {
|
|||||||
shouldRetryOnError: defaultShouldRetry,
|
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.
|
* Default predicate function to determine if a retry should be attempted.
|
||||||
* Retries on 429 (Too Many Requests) and 5xx server errors.
|
* Retries on 429 (Too Many Requests) and 5xx server errors.
|
||||||
@@ -52,12 +83,17 @@ function defaultShouldRetry(
|
|||||||
error: Error | unknown,
|
error: Error | unknown,
|
||||||
retryFetchErrors?: boolean,
|
retryFetchErrors?: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
if (retryFetchErrors && error instanceof Error) {
|
||||||
retryFetchErrors &&
|
// Check for generic fetch failed message (case-insensitive)
|
||||||
error instanceof Error &&
|
if (error.message.toLowerCase().includes(FETCH_FAILED_MESSAGE)) {
|
||||||
error.message.includes(FETCH_FAILED_MESSAGE)
|
return true;
|
||||||
) {
|
}
|
||||||
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
|
// Priority check for ApiError
|
||||||
|
|||||||
Reference in New Issue
Block a user