feat(core): increase fetch timeout and fix [object Object] error stringification (#20441)

Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
Bryan Morgan
2026-02-26 18:41:09 -05:00
committed by GitHub
parent b8d6041d42
commit 6dc9d5ff11
4 changed files with 100 additions and 24 deletions
+26 -22
View File
@@ -73,17 +73,19 @@ describe('CodeAssistServer', () => {
LlmRole.MAIN, LlmRole.MAIN,
); );
expect(mockRequest).toHaveBeenCalledWith({ expect(mockRequest).toHaveBeenCalledWith(
url: expect.stringContaining(':generateContent'), expect.objectContaining({
method: 'POST', url: expect.stringContaining(':generateContent'),
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
'x-custom-header': 'test-value', 'Content-Type': 'application/json',
}, 'x-custom-header': 'test-value',
responseType: 'json', },
body: expect.any(String), responseType: 'json',
signal: undefined, body: expect.any(String),
}); signal: undefined,
}),
);
const requestBody = JSON.parse(mockRequest.mock.calls[0][0].body); const requestBody = JSON.parse(mockRequest.mock.calls[0][0].body);
expect(requestBody.user_prompt_id).toBe('user-prompt-id'); expect(requestBody.user_prompt_id).toBe('user-prompt-id');
@@ -391,17 +393,19 @@ describe('CodeAssistServer', () => {
results.push(res); results.push(res);
} }
expect(mockRequest).toHaveBeenCalledWith({ expect(mockRequest).toHaveBeenCalledWith(
url: expect.stringContaining(':streamGenerateContent'), expect.objectContaining({
method: 'POST', url: expect.stringContaining(':streamGenerateContent'),
params: { alt: 'sse' }, method: 'POST',
responseType: 'stream', params: { alt: 'sse' },
body: expect.any(String), responseType: 'stream',
headers: { body: expect.any(String),
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json',
signal: undefined, },
}); signal: undefined,
}),
);
expect(results).toHaveLength(2); expect(results).toHaveLength(2);
expect(results[0].candidates?.[0].content?.parts?.[0].text).toBe('Hello'); expect(results[0].candidates?.[0].content?.parts?.[0].text).toBe('Hello');
+9
View File
@@ -29,6 +29,15 @@ export function getErrorMessage(error: unknown): string {
if (friendlyError instanceof Error) { if (friendlyError instanceof Error) {
return friendlyError.message; 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 { try {
return String(friendlyError); return String(friendlyError);
} catch { } catch {
@@ -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]');
});
});
+19 -2
View File
@@ -6,7 +6,18 @@
import { getErrorMessage, isNodeError } from './errors.js'; import { getErrorMessage, isNodeError } from './errors.js';
import { URL } from 'node:url'; 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 = [ const PRIVATE_IP_RANGES = [
/^10\./, /^10\./,
@@ -73,5 +84,11 @@ export async function fetchWithTimeout(
} }
export function setGlobalProxy(proxy: string) { export function setGlobalProxy(proxy: string) {
setGlobalDispatcher(new ProxyAgent(proxy)); setGlobalDispatcher(
new ProxyAgent({
uri: proxy,
headersTimeout: DEFAULT_HEADERS_TIMEOUT,
bodyTimeout: DEFAULT_BODY_TIMEOUT,
}),
);
} }