-
Notifications
You must be signed in to change notification settings - Fork 200
Fix bedrock DNS resolution when behind a corporate proxy #906
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cfd7c96
204a5da
39fd27c
2c3fd28
e224b0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,22 @@ 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("http-proxy-agent", () => ({ | ||
| HttpProxyAgent: 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 +62,18 @@ 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 { HttpProxyAgent } from "http-proxy-agent" | ||
| 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 mockHttpProxyAgent = vi.mocked(HttpProxyAgent) | ||
| const mockHttpsProxyAgent = vi.mocked(HttpsProxyAgent) | ||
|
|
||
| describe("AwsBedrockHandler", () => { | ||
| let handler: AwsBedrockHandler | ||
|
|
@@ -118,6 +142,95 @@ describe("AwsBedrockHandler", () => { | |
| }) | ||
| }) | ||
|
|
||
| describe("proxy configuration", () => { | ||
| afterEach(() => { | ||
| mockGetSystemProxyUrl.mockReturnValue(undefined) | ||
| }) | ||
|
|
||
| it("should configure NodeHttpHandler with HttpProxyAgent and 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", | ||
| }) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
| // Verify both proxy agents were created with the correct URL | ||
| expect(mockHttpProxyAgent).toHaveBeenCalledWith("http://proxy.corp.local:3128") | ||
| expect(mockHttpsProxyAgent).toHaveBeenCalledWith("http://proxy.corp.local:3128") | ||
|
|
||
| // Verify NodeHttpHandler was created with both agents | ||
| expect(mockNodeHttpHandler).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| httpAgent: expect.anything(), | ||
| httpsAgent: expect.anything(), | ||
| requestTimeout: 0, | ||
| }), | ||
| ) | ||
|
|
||
| // Verify requestHandler was set on BedrockRuntimeClient config | ||
| 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, | ||
| }), | ||
| ) | ||
| }) | ||
|
|
||
| it("should pass a custom endpoint to getSystemProxyUrl for NO_PROXY matching", () => { | ||
| new AwsBedrockHandler({ | ||
| apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", | ||
| awsAccessKey: "test-access-key", | ||
| awsSecretKey: "test-secret-key", | ||
| awsRegion: "us-east-1", | ||
| awsBedrockEndpoint: "https://bedrock.vpce.internal", | ||
| awsBedrockEndpointEnabled: true, | ||
| }) | ||
|
|
||
| expect(mockGetSystemProxyUrl).toHaveBeenCalledWith("https://bedrock.vpce.internal") | ||
| }) | ||
|
|
||
| it("should pass undefined to getSystemProxyUrl when no custom endpoint is set", () => { | ||
| new AwsBedrockHandler({ | ||
| apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", | ||
| awsAccessKey: "test-access-key", | ||
| awsSecretKey: "test-secret-key", | ||
| awsRegion: "us-east-1", | ||
| }) | ||
|
|
||
| expect(mockGetSystemProxyUrl).toHaveBeenCalledWith(undefined) | ||
| }) | ||
| }) | ||
|
|
||
| describe("region mapping and cross-region inference", () => { | ||
| describe("getPrefixForRegion", () => { | ||
| it("should return correct prefix for US regions", () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,9 +10,12 @@ 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 { HttpProxyAgent } from "http-proxy-agent" | ||
| import { HttpsProxyAgent } from "https-proxy-agent" | ||
|
|
||
| import { | ||
| type ModelInfo, | ||
|
|
@@ -44,6 +47,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, CompletePromptOptions } from "../index" | ||
|
|
||
| /************************************************************************************ | ||
|
|
@@ -294,6 +298,25 @@ 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. HttpProxyAgent and | ||
| // HttpsProxyAgent use CONNECT tunneling so the proxy handles DNS resolution instead. | ||
| // | ||
| // A custom endpoint (e.g. a VPC endpoint) is passed so NO_PROXY can bypass the proxy | ||
| // for directly-reachable hosts. For the default managed endpoint we don't reconstruct | ||
| // the hostname (the AWS SDK resolves it internally, and it varies by partition), so the | ||
| // proxy always applies there. | ||
| const proxyUrl = getSystemProxyUrl( | ||
| typeof clientConfig.endpoint === "string" ? clientConfig.endpoint : undefined, | ||
| ) | ||
| if (proxyUrl) { | ||
| clientConfig.requestHandler = new NodeHttpHandler({ | ||
| httpAgent: new HttpProxyAgent(proxyUrl), | ||
| httpsAgent: new HttpsProxyAgent(proxyUrl), | ||
| requestTimeout: 0, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ?