Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/itchy-moles-thank.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not required - need to update our docs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I need to remove this file from this PR ?

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"zoo-code": patch
---

Fix bedrock DNS resolution when behind corporate proxy
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions src/api/providers/__tests__/bedrock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect.anything() for httpsAgent matches any object or mock. Consider asserting expect.objectContaining({ httpsAgent: expect.any(Object) }) or checking against the specific mockHttpsProxyAgent instance to ensure the created agent is passed through.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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", () => {
Expand Down
14 changes: 14 additions & 0 deletions src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"

/************************************************************************************
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NodeHttpHandler is configured with only httpsAgent. If awsBedrockEndpoint is set to an http:// URL (such as an internal proxy or local mock), NodeHttpHandler selects httpAgent (which is undefined) and bypasses the proxy entirely. Consider configuring httpAgent alongside httpsAgent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it should be fixed in the last revision

})
}

this.client = new BedrockRuntimeClient(clientConfig)
}

Expand Down
2 changes: 2 additions & 0 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@
"@anthropic-ai/vertex-sdk": "^0.19.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",
Expand All @@ -486,6 +487,7 @@
"get-folder-size": "^5.0.0",
"global-agent": "^3.0.0",
"google-auth-library": "^10.2.0",
"https-proxy-agent": "^7.0.0",
"gray-matter": "^4.0.3",
"i18next": "^25.0.0",
"ignore": "^7.0.3",
Expand Down
24 changes: 24 additions & 0 deletions src/utils/networkProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSystemProxyUrl() does not evaluate NO_PROXY / no_proxy environment variables. If a user sets NO_PROXY to bypass corporate proxies for direct VPC or AWS endpoints, Bedrock requests will still be routed through the proxy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, thanks
This has been added in the last revision

// 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Fall back to VS Code's http.proxy setting
try {
const vsCodeProxy = vscode.workspace.getConfiguration("http").get<string>("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.
*/
Expand Down
Loading