mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
This commit is contained in:
@@ -409,6 +409,87 @@ describe('retryWithBackoff', () => {
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('success');
|
||||
});
|
||||
|
||||
it('should retry on SSL error code (ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC)', async () => {
|
||||
const error = new Error('SSL error');
|
||||
(error as any).code = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(error)
|
||||
.mockResolvedValue('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
initialDelayMs: 1,
|
||||
maxDelayMs: 1,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('success');
|
||||
expect(mockFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should retry on SSL error code in deeply nested cause chain', async () => {
|
||||
const deepCause = new Error('OpenSSL error');
|
||||
(deepCause as any).code = 'ERR_SSL_BAD_RECORD_MAC';
|
||||
|
||||
const middleCause = new Error('TLS handshake failed');
|
||||
(middleCause as any).cause = deepCause;
|
||||
|
||||
const outerError = new Error('fetch failed');
|
||||
(outerError as any).cause = middleCause;
|
||||
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(outerError)
|
||||
.mockResolvedValue('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
initialDelayMs: 1,
|
||||
maxDelayMs: 1,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('success');
|
||||
expect(mockFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should retry on EPROTO error (generic protocol/SSL error)', async () => {
|
||||
const error = new Error('Protocol error');
|
||||
(error as any).code = 'EPROTO';
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(error)
|
||||
.mockResolvedValue('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
initialDelayMs: 1,
|
||||
maxDelayMs: 1,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('success');
|
||||
expect(mockFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should retry on gaxios-style SSL error with code property', async () => {
|
||||
// This matches the exact structure from issue #17318
|
||||
const error = new Error(
|
||||
'request to https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent failed',
|
||||
);
|
||||
(error as any).type = 'system';
|
||||
(error as any).errno = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
|
||||
(error as any).code = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC';
|
||||
|
||||
const mockFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(error)
|
||||
.mockResolvedValue('success');
|
||||
|
||||
const promise = retryWithBackoff(mockFn, {
|
||||
initialDelayMs: 1,
|
||||
maxDelayMs: 1,
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe('success');
|
||||
expect(mockFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Flash model fallback for OAuth users', () => {
|
||||
|
||||
@@ -54,6 +54,12 @@ const RETRYABLE_NETWORK_CODES = [
|
||||
'ENOTFOUND',
|
||||
'EAI_AGAIN',
|
||||
'ECONNREFUSED',
|
||||
// SSL/TLS transient errors
|
||||
'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC',
|
||||
'ERR_SSL_WRONG_VERSION_NUMBER',
|
||||
'ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC',
|
||||
'ERR_SSL_BAD_RECORD_MAC',
|
||||
'EPROTO', // Generic protocol error (often SSL-related)
|
||||
];
|
||||
|
||||
function getNetworkErrorCode(error: unknown): string | undefined {
|
||||
@@ -72,8 +78,22 @@ function getNetworkErrorCode(error: unknown): string | undefined {
|
||||
return directCode;
|
||||
}
|
||||
|
||||
if (typeof error === 'object' && error !== null && 'cause' in error) {
|
||||
return getCode((error as { cause: unknown }).cause);
|
||||
// Traverse the cause chain to find error codes (SSL errors are often nested)
|
||||
let current: unknown = error;
|
||||
const maxDepth = 5; // Prevent infinite loops in case of circular references
|
||||
for (let depth = 0; depth < maxDepth; depth++) {
|
||||
if (
|
||||
typeof current !== 'object' ||
|
||||
current === null ||
|
||||
!('cause' in current)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
current = (current as { cause: unknown }).cause;
|
||||
const code = getCode(current);
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user