Skip to content

Commit 4e9543f

Browse files
authored
Fix bedrock DNS resolution when behind a corporate proxy (#906)
* 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. * test: add coverage for getSystemProxyUrl function * fix: honor NO_PROXY, support http proxy, trim proxy env vars
1 parent 2d2a238 commit 4e9543f

7 files changed

Lines changed: 391 additions & 1 deletion

File tree

.changeset/itchy-moles-thank.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"zoo-code": patch
3+
---
4+
5+
Fix bedrock DNS resolution when behind corporate proxy

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/providers/__tests__/bedrock.spec.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ vi.mock("@aws-sdk/credential-providers", () => {
1818
return { fromIni: mockFromIni }
1919
})
2020

21+
vi.mock("../../../utils/networkProxy", () => ({
22+
getSystemProxyUrl: vi.fn().mockReturnValue(undefined),
23+
}))
24+
25+
vi.mock("@smithy/node-http-handler", () => ({
26+
NodeHttpHandler: vi.fn(),
27+
}))
28+
29+
vi.mock("http-proxy-agent", () => ({
30+
HttpProxyAgent: vi.fn(),
31+
}))
32+
33+
vi.mock("https-proxy-agent", () => ({
34+
HttpsProxyAgent: vi.fn(),
35+
}))
36+
2137
// Mock BedrockRuntimeClient and ConverseStreamCommand
2238
vi.mock("@aws-sdk/client-bedrock-runtime", () => {
2339
const mockSend = vi.fn().mockResolvedValue({
@@ -46,10 +62,18 @@ import {
4662
} from "@roo-code/types"
4763

4864
import type { Anthropic } from "@anthropic-ai/sdk"
65+
import { getSystemProxyUrl } from "../../../utils/networkProxy"
66+
import { NodeHttpHandler } from "@smithy/node-http-handler"
67+
import { HttpProxyAgent } from "http-proxy-agent"
68+
import { HttpsProxyAgent } from "https-proxy-agent"
4969

5070
// Get access to the mocked functions
5171
const mockConverseStreamCommand = vi.mocked(ConverseStreamCommand)
5272
const mockBedrockRuntimeClient = vi.mocked(BedrockRuntimeClient)
73+
const mockGetSystemProxyUrl = vi.mocked(getSystemProxyUrl)
74+
const mockNodeHttpHandler = vi.mocked(NodeHttpHandler)
75+
const mockHttpProxyAgent = vi.mocked(HttpProxyAgent)
76+
const mockHttpsProxyAgent = vi.mocked(HttpsProxyAgent)
5377

5478
describe("AwsBedrockHandler", () => {
5579
let handler: AwsBedrockHandler
@@ -118,6 +142,95 @@ describe("AwsBedrockHandler", () => {
118142
})
119143
})
120144

145+
describe("proxy configuration", () => {
146+
afterEach(() => {
147+
mockGetSystemProxyUrl.mockReturnValue(undefined)
148+
})
149+
150+
it("should configure NodeHttpHandler with HttpProxyAgent and HttpsProxyAgent when proxy URL is set", () => {
151+
mockGetSystemProxyUrl.mockReturnValue("http://proxy.corp.local:3128")
152+
153+
new AwsBedrockHandler({
154+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
155+
awsAccessKey: "test-access-key",
156+
awsSecretKey: "test-secret-key",
157+
awsRegion: "us-east-1",
158+
})
159+
160+
// Verify both proxy agents were created with the correct URL
161+
expect(mockHttpProxyAgent).toHaveBeenCalledWith("http://proxy.corp.local:3128")
162+
expect(mockHttpsProxyAgent).toHaveBeenCalledWith("http://proxy.corp.local:3128")
163+
164+
// Verify NodeHttpHandler was created with both agents
165+
expect(mockNodeHttpHandler).toHaveBeenCalledWith(
166+
expect.objectContaining({
167+
httpAgent: expect.anything(),
168+
httpsAgent: expect.anything(),
169+
requestTimeout: 0,
170+
}),
171+
)
172+
173+
// Verify requestHandler was set on BedrockRuntimeClient config
174+
expect(mockBedrockRuntimeClient).toHaveBeenLastCalledWith(
175+
expect.objectContaining({ requestHandler: expect.anything() }),
176+
)
177+
})
178+
179+
it("should not create a proxy requestHandler when no proxy is configured", () => {
180+
new AwsBedrockHandler({
181+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
182+
awsAccessKey: "test-access-key",
183+
awsSecretKey: "test-secret-key",
184+
awsRegion: "us-east-1",
185+
})
186+
187+
expect(mockNodeHttpHandler).not.toHaveBeenCalled()
188+
expect(mockBedrockRuntimeClient.mock.lastCall?.[0]?.requestHandler).toBeUndefined()
189+
})
190+
191+
it("should apply proxy for API key authentication", () => {
192+
mockGetSystemProxyUrl.mockReturnValue("http://proxy.corp.local:3128")
193+
194+
new AwsBedrockHandler({
195+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
196+
awsUseApiKey: true,
197+
awsApiKey: "test-api-key",
198+
awsRegion: "us-east-1",
199+
})
200+
201+
expect(mockNodeHttpHandler).toHaveBeenCalledWith(
202+
expect.objectContaining({
203+
httpsAgent: expect.anything(),
204+
requestTimeout: 0,
205+
}),
206+
)
207+
})
208+
209+
it("should pass a custom endpoint to getSystemProxyUrl for NO_PROXY matching", () => {
210+
new AwsBedrockHandler({
211+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
212+
awsAccessKey: "test-access-key",
213+
awsSecretKey: "test-secret-key",
214+
awsRegion: "us-east-1",
215+
awsBedrockEndpoint: "https://bedrock.vpce.internal",
216+
awsBedrockEndpointEnabled: true,
217+
})
218+
219+
expect(mockGetSystemProxyUrl).toHaveBeenCalledWith("https://bedrock.vpce.internal")
220+
})
221+
222+
it("should pass undefined to getSystemProxyUrl when no custom endpoint is set", () => {
223+
new AwsBedrockHandler({
224+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
225+
awsAccessKey: "test-access-key",
226+
awsSecretKey: "test-secret-key",
227+
awsRegion: "us-east-1",
228+
})
229+
230+
expect(mockGetSystemProxyUrl).toHaveBeenCalledWith(undefined)
231+
})
232+
})
233+
121234
describe("region mapping and cross-region inference", () => {
122235
describe("getPrefixForRegion", () => {
123236
it("should return correct prefix for US regions", () => {

src/api/providers/bedrock.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import {
1010
ToolConfiguration,
1111
ToolChoice,
1212
} from "@aws-sdk/client-bedrock-runtime"
13+
import { NodeHttpHandler } from "@smithy/node-http-handler"
1314
import OpenAI from "openai"
1415
import { fromIni } from "@aws-sdk/credential-providers"
1516
import { Anthropic } from "@anthropic-ai/sdk"
17+
import { HttpProxyAgent } from "http-proxy-agent"
18+
import { HttpsProxyAgent } from "https-proxy-agent"
1619

1720
import {
1821
type ModelInfo,
@@ -44,6 +47,7 @@ import { convertToBedrockConverseMessages as sharedConverter } from "../transfor
4447
import { getModelParams } from "../transform/model-params"
4548
import { shouldUseReasoningBudget } from "../../shared/api"
4649
import { normalizeToolSchema } from "../../utils/json-schema"
50+
import { getSystemProxyUrl } from "../../utils/networkProxy"
4751
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"
4852

4953
/************************************************************************************
@@ -294,6 +298,25 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
294298
}
295299
}
296300

301+
// When a corporate proxy is configured, Node resolves DNS locally before tunneling,
302+
// causing ENOTFOUND for endpoints that only the proxy can reach. HttpProxyAgent and
303+
// HttpsProxyAgent use CONNECT tunneling so the proxy handles DNS resolution instead.
304+
//
305+
// A custom endpoint (e.g. a VPC endpoint) is passed so NO_PROXY can bypass the proxy
306+
// for directly-reachable hosts. For the default managed endpoint we don't reconstruct
307+
// the hostname (the AWS SDK resolves it internally, and it varies by partition), so the
308+
// proxy always applies there.
309+
const proxyUrl = getSystemProxyUrl(
310+
typeof clientConfig.endpoint === "string" ? clientConfig.endpoint : undefined,
311+
)
312+
if (proxyUrl) {
313+
clientConfig.requestHandler = new NodeHttpHandler({
314+
httpAgent: new HttpProxyAgent(proxyUrl),
315+
httpsAgent: new HttpsProxyAgent(proxyUrl),
316+
requestTimeout: 0,
317+
})
318+
}
319+
297320
this.client = new BedrockRuntimeClient(clientConfig)
298321
}
299322

src/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@
461461
"@anthropic-ai/vertex-sdk": "^0.19.0",
462462
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
463463
"@aws-sdk/credential-providers": "^3.922.0",
464+
"@smithy/node-http-handler": "^4.0.0",
464465
"@google/genai": "^1.29.1",
465466
"@lmstudio/sdk": "^1.1.1",
466467
"@mistralai/mistralai": "^1.9.18",
@@ -486,6 +487,8 @@
486487
"get-folder-size": "^5.0.0",
487488
"global-agent": "^3.0.0",
488489
"google-auth-library": "^10.2.0",
490+
"http-proxy-agent": "^7.0.0",
491+
"https-proxy-agent": "^7.0.0",
489492
"gray-matter": "^4.0.3",
490493
"i18next": "^25.0.0",
491494
"ignore": "^7.0.3",

0 commit comments

Comments
 (0)