fix(core): add retry logic for specific fetch errors (#11066)

This commit is contained in:
Sandy Tao
2025-10-14 09:17:31 -07:00
committed by GitHub
parent c86ee4cc83
commit 7c1a90244a
8 changed files with 143 additions and 3 deletions
+35
View File
@@ -304,6 +304,41 @@ 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.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.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 promise = retryWithBackoff(mockFn, {
retryFetchErrors,
});
await expect(promise).rejects.toThrow(fetchErrorMsg);
expect(mockFn).toHaveBeenCalledTimes(1);
},
);
});
describe('Flash model fallback for OAuth users', () => {
it('should trigger fallback for OAuth personal users on TerminalQuotaError', async () => {
const fallbackCallback = vi.fn().mockResolvedValue('gemini-2.5-flash');
+23 -3
View File
@@ -13,6 +13,9 @@ import {
TerminalQuotaError,
} from './googleQuotaErrors.js';
const FETCH_FAILED_MESSAGE =
'exception TypeError: fetch failed sending request';
export interface HttpError extends Error {
status?: number;
}
@@ -21,13 +24,14 @@ export interface RetryOptions {
maxAttempts: number;
initialDelayMs: number;
maxDelayMs: number;
shouldRetryOnError: (error: Error) => boolean;
shouldRetryOnError: (error: Error, retryFetchErrors?: boolean) => boolean;
shouldRetryOnContent?: (content: GenerateContentResponse) => boolean;
onPersistent429?: (
authType?: string,
error?: unknown,
) => Promise<string | boolean | null>;
authType?: string;
retryFetchErrors?: boolean;
}
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
@@ -41,9 +45,21 @@ const DEFAULT_RETRY_OPTIONS: RetryOptions = {
* Default predicate function to determine if a retry should be attempted.
* Retries on 429 (Too Many Requests) and 5xx server errors.
* @param error The error object.
* @param retryFetchErrors Whether to retry on specific fetch errors.
* @returns True if the error is a transient error, false otherwise.
*/
function defaultShouldRetry(error: Error | unknown): boolean {
function defaultShouldRetry(
error: Error | unknown,
retryFetchErrors?: boolean,
): boolean {
if (
retryFetchErrors &&
error instanceof Error &&
error.message.includes(FETCH_FAILED_MESSAGE)
) {
return true;
}
// Priority check for ApiError
if (error instanceof ApiError) {
// Explicitly do not retry 400 (Bad Request)
@@ -96,6 +112,7 @@ export async function retryWithBackoff<T>(
authType,
shouldRetryOnError,
shouldRetryOnContent,
retryFetchErrors,
} = {
...DEFAULT_RETRY_OPTIONS,
...cleanOptions,
@@ -155,7 +172,10 @@ export async function retryWithBackoff<T>(
}
// Generic retry logic for other errors
if (attempt >= maxAttempts || !shouldRetryOnError(error as Error)) {
if (
attempt >= maxAttempts ||
!shouldRetryOnError(error as Error, retryFetchErrors)
) {
throw error;
}