diff --git a/packages/core/src/utils/fetch.test.ts b/packages/core/src/utils/fetch.test.ts index e4da21ffa0..1f56f7af19 100644 --- a/packages/core/src/utils/fetch.test.ts +++ b/packages/core/src/utils/fetch.test.ts @@ -209,7 +209,7 @@ describe('fetch utils', () => { expect(ProxyAgent).toHaveBeenCalledWith({ uri: proxyUrl, headersTimeout: 45773134, - bodyTimeout: 45773134, + bodyTimeout: 300000, }); expect(setGlobalDispatcher).toHaveBeenCalled(); }); diff --git a/packages/core/src/utils/fetch.ts b/packages/core/src/utils/fetch.ts index 755875ff75..8c2fddc868 100644 --- a/packages/core/src/utils/fetch.ts +++ b/packages/core/src/utils/fetch.ts @@ -28,14 +28,15 @@ export class PrivateIpError extends Error { } } -let defaultTimeout = 300000; // 5 minutes +let defaultHeadersTimeout = 60000; // 60 seconds +const defaultBodyTimeout = 300000; // 5 minutes let currentProxy: string | undefined = undefined; // Configure default global dispatcher with higher timeouts setGlobalDispatcher( new Agent({ - headersTimeout: defaultTimeout, - bodyTimeout: defaultTimeout, + headersTimeout: defaultHeadersTimeout, + bodyTimeout: defaultBodyTimeout, }), ); @@ -45,14 +46,15 @@ export function updateGlobalFetchTimeouts(timeoutMs: number) { `Invalid timeout value: ${timeoutMs}. Must be a positive finite number.`, ); } - defaultTimeout = timeoutMs; + defaultHeadersTimeout = timeoutMs; + // We keep body timeout high for LLM streaming responses if (currentProxy) { setGlobalProxy(currentProxy); } else { setGlobalDispatcher( new Agent({ - headersTimeout: defaultTimeout, - bodyTimeout: defaultTimeout, + headersTimeout: defaultHeadersTimeout, + bodyTimeout: defaultBodyTimeout, }), ); } @@ -214,8 +216,8 @@ export function setGlobalProxy(proxy: string) { setGlobalDispatcher( new ProxyAgent({ uri: proxy, - headersTimeout: defaultTimeout, - bodyTimeout: defaultTimeout, + headersTimeout: defaultHeadersTimeout, + bodyTimeout: defaultBodyTimeout, }), ); } diff --git a/packages/core/src/utils/retry.test.ts b/packages/core/src/utils/retry.test.ts index 29758e6e92..290f14eadb 100644 --- a/packages/core/src/utils/retry.test.ts +++ b/packages/core/src/utils/retry.test.ts @@ -451,6 +451,25 @@ describe('retryWithBackoff', () => { }); await vi.runAllTimersAsync(); await expect(promise).resolves.toBe('success'); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it('should retry on undici timeout error codes (UND_ERR_HEADERS_TIMEOUT)', async () => { + const error = new Error('Headers timeout error'); + (error as any).code = 'UND_ERR_HEADERS_TIMEOUT'; + const mockFn = vi + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue('success'); + + const promise = retryWithBackoff(mockFn, { + retryFetchErrors: false, + initialDelayMs: 1, + maxDelayMs: 1, + }); + await vi.runAllTimersAsync(); + await expect(promise).resolves.toBe('success'); + expect(mockFn).toHaveBeenCalledTimes(2); }); it('should retry on SSL error code (ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC)', async () => { diff --git a/packages/core/src/utils/retry.ts b/packages/core/src/utils/retry.ts index 5b3ac4f113..9478d0f2c2 100644 --- a/packages/core/src/utils/retry.ts +++ b/packages/core/src/utils/retry.ts @@ -55,6 +55,9 @@ const RETRYABLE_NETWORK_CODES = [ 'ECONNREFUSED', 'ERR_SSL_WRONG_VERSION_NUMBER', 'EPROTO', // Generic protocol error (often SSL-related) + 'UND_ERR_HEADERS_TIMEOUT', + 'UND_ERR_BODY_TIMEOUT', + 'UND_ERR_CONNECT_TIMEOUT', ]; // Node.js builds SSL error codes by prepending ERR_SSL_ to the uppercased