From 6dc9d5ff11c31b8da829ffca91c8e544c3e7bc07 Mon Sep 17 00:00:00 2001 From: Bryan Morgan Date: Thu, 26 Feb 2026 18:41:09 -0500 Subject: [PATCH] feat(core): increase fetch timeout and fix [object Object] error stringification (#20441) Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> --- packages/core/src/code_assist/server.test.ts | 48 ++++++++++--------- packages/core/src/utils/errors.ts | 9 ++++ .../core/src/utils/errors_timeout.test.ts | 46 ++++++++++++++++++ packages/core/src/utils/fetch.ts | 21 +++++++- 4 files changed, 100 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/utils/errors_timeout.test.ts diff --git a/packages/core/src/code_assist/server.test.ts b/packages/core/src/code_assist/server.test.ts index 8ec8cb8dad..d79526d1c3 100644 --- a/packages/core/src/code_assist/server.test.ts +++ b/packages/core/src/code_assist/server.test.ts @@ -73,17 +73,19 @@ describe('CodeAssistServer', () => { LlmRole.MAIN, ); - expect(mockRequest).toHaveBeenCalledWith({ - url: expect.stringContaining(':generateContent'), - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-custom-header': 'test-value', - }, - responseType: 'json', - body: expect.any(String), - signal: undefined, - }); + expect(mockRequest).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining(':generateContent'), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-custom-header': 'test-value', + }, + responseType: 'json', + body: expect.any(String), + signal: undefined, + }), + ); const requestBody = JSON.parse(mockRequest.mock.calls[0][0].body); expect(requestBody.user_prompt_id).toBe('user-prompt-id'); @@ -391,17 +393,19 @@ describe('CodeAssistServer', () => { results.push(res); } - expect(mockRequest).toHaveBeenCalledWith({ - url: expect.stringContaining(':streamGenerateContent'), - method: 'POST', - params: { alt: 'sse' }, - responseType: 'stream', - body: expect.any(String), - headers: { - 'Content-Type': 'application/json', - }, - signal: undefined, - }); + expect(mockRequest).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining(':streamGenerateContent'), + method: 'POST', + params: { alt: 'sse' }, + responseType: 'stream', + body: expect.any(String), + headers: { + 'Content-Type': 'application/json', + }, + signal: undefined, + }), + ); expect(results).toHaveLength(2); expect(results[0].candidates?.[0].content?.parts?.[0].text).toBe('Hello'); diff --git a/packages/core/src/utils/errors.ts b/packages/core/src/utils/errors.ts index 5465977ff2..62db5dcbf4 100644 --- a/packages/core/src/utils/errors.ts +++ b/packages/core/src/utils/errors.ts @@ -29,6 +29,15 @@ export function getErrorMessage(error: unknown): string { if (friendlyError instanceof Error) { return friendlyError.message; } + if ( + typeof friendlyError === 'object' && + friendlyError !== null && + 'message' in friendlyError && + typeof (friendlyError as { message: unknown }).message === 'string' + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + return (friendlyError as { message: string }).message; + } try { return String(friendlyError); } catch { diff --git a/packages/core/src/utils/errors_timeout.test.ts b/packages/core/src/utils/errors_timeout.test.ts new file mode 100644 index 0000000000..54eda960a0 --- /dev/null +++ b/packages/core/src/utils/errors_timeout.test.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { getErrorMessage } from './errors.js'; +import { type HttpError } from './httpErrors.js'; + +describe('getErrorMessage with timeout errors', () => { + it('should handle undici HeadersTimeoutError correctly', () => { + // Simulate what undici might throw if it's not a proper Error instance + // or has a specific code. + const timeoutError = { + name: 'HeadersTimeoutError', + code: 'UND_ERR_HEADERS_TIMEOUT', + message: 'Headers timeout error', + }; + + // If it's a plain object, getErrorMessage might struggle if it expects an Error + const message = getErrorMessage(timeoutError); + // Based on existing implementation: + // friendlyError = toFriendlyError(timeoutError) -> returns timeoutError + // if (friendlyError instanceof Error) -> false + // return String(friendlyError) -> "[object Object]" + + expect(message).toBe('Headers timeout error'); + }); + + it('should handle undici HeadersTimeoutError as an Error instance', () => { + const error = new Error('Headers timeout error'); + (error as HttpError).name = 'HeadersTimeoutError'; + (error as HttpError).status = 504; // simulate status for test + (error as HttpError & { code?: string }).code = 'UND_ERR_HEADERS_TIMEOUT'; + + const message = getErrorMessage(error); + expect(message).toBe('Headers timeout error'); + }); + + it('should return String representation for objects without a message property', () => { + const error = { some: 'other', object: 123 }; + const message = getErrorMessage(error); + expect(message).toBe('[object Object]'); + }); +}); diff --git a/packages/core/src/utils/fetch.ts b/packages/core/src/utils/fetch.ts index 30d583e99f..e0bb1f3378 100644 --- a/packages/core/src/utils/fetch.ts +++ b/packages/core/src/utils/fetch.ts @@ -6,7 +6,18 @@ import { getErrorMessage, isNodeError } from './errors.js'; import { URL } from 'node:url'; -import { ProxyAgent, setGlobalDispatcher } from 'undici'; +import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici'; + +const DEFAULT_HEADERS_TIMEOUT = 60000; // 60 seconds +const DEFAULT_BODY_TIMEOUT = 300000; // 5 minutes + +// Configure default global dispatcher with higher timeouts +setGlobalDispatcher( + new Agent({ + headersTimeout: DEFAULT_HEADERS_TIMEOUT, + bodyTimeout: DEFAULT_BODY_TIMEOUT, + }), +); const PRIVATE_IP_RANGES = [ /^10\./, @@ -73,5 +84,11 @@ export async function fetchWithTimeout( } export function setGlobalProxy(proxy: string) { - setGlobalDispatcher(new ProxyAgent(proxy)); + setGlobalDispatcher( + new ProxyAgent({ + uri: proxy, + headersTimeout: DEFAULT_HEADERS_TIMEOUT, + bodyTimeout: DEFAULT_BODY_TIMEOUT, + }), + ); }