diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index dc743d9b9..b25690092 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -108,6 +108,7 @@ import { shouldDefaultToNodePty } from '../utils/shell-utils.js'; import { WorkspaceContext } from '../utils/workspaceContext.js'; import { type ToolName } from '../utils/tool-utils.js'; import { getErrorMessage } from '../utils/errors.js'; +import { normalizeProxyUrl } from '../utils/proxyUtils.js'; // Local config modules import type { FileFilteringOptions } from './constants.js'; @@ -747,8 +748,9 @@ export class Config { initializeTelemetry(this); } - if (this.getProxy()) { - setGlobalDispatcher(new ProxyAgent(this.getProxy() as string)); + const normalizedProxy = normalizeProxyUrl(this.getProxy()); + if (normalizedProxy) { + setGlobalDispatcher(new ProxyAgent(normalizedProxy)); } this.geminiClient = new GeminiClient(this); this.chatRecordingService = this.chatRecordingEnabled diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 66359a865..83ab203ca 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -217,6 +217,7 @@ export * from './utils/pathReader.js'; export * from './utils/paths.js'; export * from './utils/projectSummary.js'; export * from './utils/promptIdContext.js'; +export * from './utils/proxyUtils.js'; export * from './utils/quotaErrorDetection.js'; export * from './utils/readManyFiles.js'; export * from './utils/request-tokenizer/supportedImageFormats.js'; diff --git a/packages/core/src/utils/proxyUtils.test.ts b/packages/core/src/utils/proxyUtils.test.ts new file mode 100644 index 000000000..750e45a69 --- /dev/null +++ b/packages/core/src/utils/proxyUtils.test.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { normalizeProxyUrl } from './proxyUtils.js'; + +describe('normalizeProxyUrl', () => { + it('should return undefined for undefined input', () => { + expect(normalizeProxyUrl(undefined)).toBeUndefined(); + }); + + it('should return undefined for empty string', () => { + expect(normalizeProxyUrl('')).toBeUndefined(); + }); + + it('should return undefined for whitespace-only string', () => { + expect(normalizeProxyUrl(' ')).toBeUndefined(); + }); + + it('should add http:// prefix to proxy URL without protocol', () => { + expect(normalizeProxyUrl('127.0.0.1:7860')).toBe('http://127.0.0.1:7860'); + }); + + it('should add http:// prefix to proxy URL with port only', () => { + expect(normalizeProxyUrl('localhost:8080')).toBe('http://localhost:8080'); + }); + + it('should not modify URL that already has http:// prefix', () => { + expect(normalizeProxyUrl('http://127.0.0.1:7860')).toBe( + 'http://127.0.0.1:7860', + ); + }); + + it('should not modify URL that already has https:// prefix', () => { + expect(normalizeProxyUrl('https://proxy.example.com:443')).toBe( + 'https://proxy.example.com:443', + ); + }); + + it('should handle HTTP:// prefix (case insensitive)', () => { + expect(normalizeProxyUrl('HTTP://127.0.0.1:7860')).toBe( + 'HTTP://127.0.0.1:7860', + ); + }); + + it('should handle HTTPS:// prefix (case insensitive)', () => { + expect(normalizeProxyUrl('HTTPS://proxy.example.com:443')).toBe( + 'HTTPS://proxy.example.com:443', + ); + }); + + it('should handle proxy URL with authentication', () => { + expect(normalizeProxyUrl('user:pass@proxy.example.com:8080')).toBe( + 'http://user:pass@proxy.example.com:8080', + ); + }); + + it('should handle proxy URL with authentication and http:// prefix', () => { + expect(normalizeProxyUrl('http://user:pass@proxy.example.com:8080')).toBe( + 'http://user:pass@proxy.example.com:8080', + ); + }); + + it('should trim whitespace from proxy URL', () => { + expect(normalizeProxyUrl(' 127.0.0.1:7860 ')).toBe( + 'http://127.0.0.1:7860', + ); + }); + + it('should handle IPv6 addresses', () => { + expect(normalizeProxyUrl('[::1]:8080')).toBe('http://[::1]:8080'); + }); + + it('should handle IPv6 addresses with http:// prefix', () => { + expect(normalizeProxyUrl('http://[::1]:8080')).toBe('http://[::1]:8080'); + }); +}); diff --git a/packages/core/src/utils/proxyUtils.ts b/packages/core/src/utils/proxyUtils.ts new file mode 100644 index 000000000..eb776ec71 --- /dev/null +++ b/packages/core/src/utils/proxyUtils.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Normalizes a proxy URL to ensure it has a valid protocol prefix. + * + * Many proxy tools and environment variables provide proxy addresses without + * a protocol prefix (e.g., "127.0.0.1:7860" instead of "http://127.0.0.1:7860"). + * This function adds the "http://" prefix if missing, since HTTP proxies are + * the most common default. + * + * @param proxyUrl - The proxy URL to normalize + * @returns The normalized proxy URL with protocol prefix, or undefined if input is undefined/empty + */ +export function normalizeProxyUrl( + proxyUrl: string | undefined, +): string | undefined { + if (!proxyUrl) { + return undefined; + } + + const trimmed = proxyUrl.trim(); + if (!trimmed) { + return undefined; + } + + // Check if the URL already has a protocol prefix + if (/^https?:\/\//i.test(trimmed)) { + return trimmed; + } + + // Add http:// prefix for proxy URLs without protocol + // HTTP is the default for most proxy configurations + return `http://${trimmed}`; +}