From cfd7c968a9d4d555955e50f8920d8f2d52934af2 Mon Sep 17 00:00:00 2001 From: LouisClt Date: Wed, 8 Jul 2026 13:21:46 +0200 Subject: [PATCH] Fix bedrock DNS resolution when behind a corporate proxy When behind a corporate proxy, Node.js resolves DNS locally before contacting the proxy. If the proxy is the only path to the endpoint, the request fails with ENOTFOUND. HttpsProxyAgent uses CONNECT tunneling so the proxy handles DNS resolution. Configure NodeHttpHandler with HttpsProxyAgent when a system proxy is detected. --- .changeset/itchy-moles-thank.md | 5 ++ pnpm-lock.yaml | 6 ++ src/api/providers/__tests__/bedrock.spec.ts | 76 +++++++++++++++++++++ src/api/providers/bedrock.ts | 14 ++++ src/package.json | 2 + src/utils/networkProxy.ts | 24 +++++++ 6 files changed, 127 insertions(+) create mode 100644 .changeset/itchy-moles-thank.md diff --git a/.changeset/itchy-moles-thank.md b/.changeset/itchy-moles-thank.md new file mode 100644 index 0000000000..42bb637024 --- /dev/null +++ b/.changeset/itchy-moles-thank.md @@ -0,0 +1,5 @@ +--- +"zoo-code": patch +--- + +Fix bedrock DNS resolution when behind corporate proxy diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35c3c52886..9828b3cbe4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -463,6 +463,9 @@ importers: '@roo-code/types': specifier: workspace:^ version: link:../packages/types + '@smithy/node-http-handler': + specifier: ^4.0.0 + version: 4.8.2 ai-sdk-provider-poe: specifier: 2.0.18 version: 2.0.18(ai@6.0.209(zod@3.25.76))(zod@3.25.76) @@ -508,6 +511,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + https-proxy-agent: + specifier: ^7.0.0 + version: 7.0.6 i18next: specifier: ^25.0.0 version: 25.2.1(typescript@5.9.3) diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index e2b17e4e87..97d3075f20 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -18,6 +18,18 @@ vi.mock("@aws-sdk/credential-providers", () => { return { fromIni: mockFromIni } }) +vi.mock("../../../utils/networkProxy", () => ({ + getSystemProxyUrl: vi.fn().mockReturnValue(undefined), +})) + +vi.mock("@smithy/node-http-handler", () => ({ + NodeHttpHandler: vi.fn(), +})) + +vi.mock("https-proxy-agent", () => ({ + HttpsProxyAgent: vi.fn(), +})) + // Mock BedrockRuntimeClient and ConverseStreamCommand vi.mock("@aws-sdk/client-bedrock-runtime", () => { const mockSend = vi.fn().mockResolvedValue({ @@ -46,10 +58,16 @@ import { } from "@roo-code/types" import type { Anthropic } from "@anthropic-ai/sdk" +import { getSystemProxyUrl } from "../../../utils/networkProxy" +import { NodeHttpHandler } from "@smithy/node-http-handler" +import { HttpsProxyAgent } from "https-proxy-agent" // Get access to the mocked functions const mockConverseStreamCommand = vi.mocked(ConverseStreamCommand) const mockBedrockRuntimeClient = vi.mocked(BedrockRuntimeClient) +const mockGetSystemProxyUrl = vi.mocked(getSystemProxyUrl) +const mockNodeHttpHandler = vi.mocked(NodeHttpHandler) +const mockHttpsProxyAgent = vi.mocked(HttpsProxyAgent) describe("AwsBedrockHandler", () => { let handler: AwsBedrockHandler @@ -118,6 +136,64 @@ describe("AwsBedrockHandler", () => { }) }) + describe("proxy configuration", () => { + afterEach(() => { + mockGetSystemProxyUrl.mockReturnValue(undefined) + }) + + it("should configure NodeHttpHandler with HttpsProxyAgent when proxy URL is set", () => { + mockGetSystemProxyUrl.mockReturnValue("http://proxy.corp.local:3128") + + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + }) + + expect(mockHttpsProxyAgent).toHaveBeenCalledWith("http://proxy.corp.local:3128") + expect(mockNodeHttpHandler).toHaveBeenCalledWith( + expect.objectContaining({ + httpsAgent: expect.anything(), + requestTimeout: 0, + }), + ) + expect(mockBedrockRuntimeClient).toHaveBeenLastCalledWith( + expect.objectContaining({ requestHandler: expect.anything() }), + ) + }) + + it("should not create a proxy requestHandler when no proxy is configured", () => { + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + }) + + expect(mockNodeHttpHandler).not.toHaveBeenCalled() + expect(mockBedrockRuntimeClient.mock.lastCall?.[0]?.requestHandler).toBeUndefined() + }) + + it("should apply proxy for API key authentication", () => { + mockGetSystemProxyUrl.mockReturnValue("http://proxy.corp.local:3128") + + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsUseApiKey: true, + awsApiKey: "test-api-key", + awsRegion: "us-east-1", + }) + + expect(mockNodeHttpHandler).toHaveBeenCalledWith( + expect.objectContaining({ + httpsAgent: expect.anything(), + requestTimeout: 0, + }), + ) + }) + }) + describe("region mapping and cross-region inference", () => { describe("getPrefixForRegion", () => { it("should return correct prefix for US regions", () => { diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 8c2b5ace68..90cb16077f 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -10,9 +10,11 @@ import { ToolConfiguration, ToolChoice, } from "@aws-sdk/client-bedrock-runtime" +import { NodeHttpHandler } from "@smithy/node-http-handler" import OpenAI from "openai" import { fromIni } from "@aws-sdk/credential-providers" import { Anthropic } from "@anthropic-ai/sdk" +import { HttpsProxyAgent } from "https-proxy-agent" import { type ModelInfo, @@ -44,6 +46,7 @@ import { convertToBedrockConverseMessages as sharedConverter } from "../transfor import { getModelParams } from "../transform/model-params" import { shouldUseReasoningBudget } from "../../shared/api" import { normalizeToolSchema } from "../../utils/json-schema" +import { getSystemProxyUrl } from "../../utils/networkProxy" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" /************************************************************************************ @@ -294,6 +297,17 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH } } + // When a corporate proxy is configured, Node resolves DNS locally before tunneling, + // causing ENOTFOUND for endpoints that only the proxy can reach. HttpsProxyAgent + // uses CONNECT tunneling so the proxy handles DNS resolution instead. + const proxyUrl = getSystemProxyUrl() + if (proxyUrl) { + clientConfig.requestHandler = new NodeHttpHandler({ + httpsAgent: new HttpsProxyAgent(proxyUrl), + requestTimeout: 0, + }) + } + this.client = new BedrockRuntimeClient(clientConfig) } diff --git a/src/package.json b/src/package.json index 389ff99885..15b334de5b 100644 --- a/src/package.json +++ b/src/package.json @@ -461,6 +461,7 @@ "@anthropic-ai/vertex-sdk": "^0.18.0", "@aws-sdk/client-bedrock-runtime": "^3.922.0", "@aws-sdk/credential-providers": "^3.922.0", + "@smithy/node-http-handler": "^4.0.0", "@google/genai": "^1.29.1", "@lmstudio/sdk": "^1.1.1", "@mistralai/mistralai": "^1.9.18", @@ -485,6 +486,7 @@ "get-folder-size": "^5.0.0", "global-agent": "^3.0.0", "google-auth-library": "^9.15.1", + "https-proxy-agent": "^7.0.0", "gray-matter": "^4.0.3", "i18next": "^25.0.0", "ignore": "^7.0.3", diff --git a/src/utils/networkProxy.ts b/src/utils/networkProxy.ts index 448bc1b576..853401eb35 100644 --- a/src/utils/networkProxy.ts +++ b/src/utils/networkProxy.ts @@ -346,6 +346,30 @@ export function isDebugMode(): boolean { return extensionContext.extensionMode === vscode.ExtensionMode.Development } +/** + * Get the proxy URL from environment variables or VS Code settings. + * Works in all extension modes (production and debug). + */ +export function getSystemProxyUrl(): string | undefined { + // Standard proxy environment variables (HTTPS takes precedence over HTTP) + const fromEnv = + process.env.HTTPS_PROXY || + process.env.https_proxy || + process.env.HTTP_PROXY || + process.env.http_proxy + if (fromEnv) return fromEnv + + // Fall back to VS Code's http.proxy setting + try { + const vsCodeProxy = vscode.workspace.getConfiguration("http").get("proxy") + if (vsCodeProxy?.trim()) return vsCodeProxy.trim() + } catch { + // VS Code API may be unavailable in test environments + } + + return undefined +} + /** * Log a message to the output channel if available. */