From dc859584a352026af00c68d23bbb256d9d4f20cb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 14:32:09 +0100 Subject: [PATCH] fix(gateway): honor all_proxy in env dispatcher --- CHANGELOG.md | 1 + docs/nodes/audio.md | 2 + docs/nodes/media-understanding.md | 2 + src/cli/run-main.exit.test.ts | 12 ++-- src/cli/run-main.ts | 4 +- src/infra/net/proxy-env.test.ts | 51 ++++++++++++++++ src/infra/net/proxy-env.ts | 36 +++++++++++ src/infra/net/proxy-fetch.test.ts | 33 ++++++++-- src/infra/net/proxy-fetch.ts | 10 +-- .../net/undici-global-dispatcher.test.ts | 61 ++++++++++++++++--- src/infra/net/undici-global-dispatcher.ts | 7 ++- 11 files changed, 188 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02fe278a512..880c34389fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai - Plugins/install: resolve plugin install destinations from the active profile state dir across CLI, ClawHub, marketplace, local path, and channel setup installs, so `openclaw --profile plugins install ...` no longer writes into the default profile. Fixes #69960; carries forward #69971. Thanks @FrancisLyman and @Sanjays2402. - Plugins/registry: suppress duplicate-plugin startup warnings when a tracked npm-installed plugin intentionally overrides the bundled plugin with the same id. Carries forward #48673. Thanks @abdushsk. - Plugins/startup: reuse canonical realpath lookups throughout each plugin discovery pass, including package and manifest boundary checks, so Windows npm-global startups no longer repeat expensive path resolution for the same plugin roots. Fixes #65733. Thanks @welfo-beo. +- Gateway/proxy: pass `ALL_PROXY` / `all_proxy` into the global Undici env-proxy dispatcher and provider proxy-fetch helper while keeping SSRF trusted-proxy auto-upgrade on `HTTP_PROXY` / `HTTPS_PROXY` only, so gateway/provider calls honor all-proxy setups without weakening guarded fetches. Fixes #43919. Thanks @RickyTong1. - Reply/link understanding: keep media and link preprocessing on stable runtime entrypoints and continue with raw message content if optional enrichment fails, so URL-bearing messages are no longer dropped after stale runtime chunk upgrades. Fixes #68466. Thanks @songshikang0111. - Discord: persist routed model-picker overrides when the hidden `/model` dispatch succeeds but the bound thread session store is still stale, including LM Studio suffixed model ids. Carries forward #61473. Thanks @Nanako0129. - Nodes/CLI: add `openclaw nodes remove --node ` and `node.pair.remove` so stale gateway-owned node pairing records can be cleaned without hand-editing state files. diff --git a/docs/nodes/audio.md b/docs/nodes/audio.md index f949156d2e8..28e852787de 100644 --- a/docs/nodes/audio.md +++ b/docs/nodes/audio.md @@ -171,8 +171,10 @@ Provider-based audio transcription honors standard outbound proxy env vars: - `HTTPS_PROXY` - `HTTP_PROXY` +- `ALL_PROXY` - `https_proxy` - `http_proxy` +- `all_proxy` If no proxy env vars are set, direct egress is used. If proxy config is malformed, OpenClaw logs a warning and falls back to direct fetch. diff --git a/docs/nodes/media-understanding.md b/docs/nodes/media-understanding.md index af4a0fd0488..1a8494cffd7 100644 --- a/docs/nodes/media-understanding.md +++ b/docs/nodes/media-understanding.md @@ -220,8 +220,10 @@ When provider-based **audio** and **video** media understanding is enabled, Open - `HTTPS_PROXY` - `HTTP_PROXY` +- `ALL_PROXY` - `https_proxy` - `http_proxy` +- `all_proxy` If no proxy env vars are set, media understanding uses direct egress. If the proxy value is malformed, OpenClaw logs a warning and falls back to direct fetch. diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index 21dbac490ab..2a8d546cb69 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -20,7 +20,7 @@ const getProgramContextMock = vi.hoisted(() => vi.fn(() => null)); const registerCoreCliByNameMock = vi.hoisted(() => vi.fn()); const registerSubCliByNameMock = vi.hoisted(() => vi.fn()); const restoreTerminalStateMock = vi.hoisted(() => vi.fn()); -const hasEnvHttpProxyConfiguredMock = vi.hoisted(() => vi.fn(() => false)); +const hasEnvHttpProxyAgentConfiguredMock = vi.hoisted(() => vi.fn(() => false)); const ensureGlobalUndiciEnvProxyDispatcherMock = vi.hoisted(() => vi.fn()); const runCrestodianMock = vi.hoisted(() => vi.fn(async () => {})); const progressDoneMock = vi.hoisted(() => vi.fn()); @@ -106,7 +106,7 @@ vi.mock("../terminal/restore.js", () => ({ })); vi.mock("../infra/net/proxy-env.js", () => ({ - hasEnvHttpProxyConfigured: hasEnvHttpProxyConfiguredMock, + hasEnvHttpProxyAgentConfigured: hasEnvHttpProxyAgentConfiguredMock, })); vi.mock("../infra/net/undici-global-dispatcher.js", () => ({ @@ -127,7 +127,7 @@ describe("runCli exit behavior", () => { hasMemoryRuntimeMock.mockReturnValue(false); outputPrecomputedBrowserHelpTextMock.mockReturnValue(false); outputPrecomputedRootHelpTextMock.mockReturnValue(false); - hasEnvHttpProxyConfiguredMock.mockReturnValue(false); + hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false); getProgramContextMock.mockReturnValue(null); delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH; }); @@ -178,7 +178,7 @@ describe("runCli exit behavior", () => { await runCli(["node", "openclaw", "--help"]); expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1); - expect(hasEnvHttpProxyConfiguredMock).not.toHaveBeenCalled(); + expect(hasEnvHttpProxyAgentConfiguredMock).not.toHaveBeenCalled(); expect(ensureGlobalUndiciEnvProxyDispatcherMock).not.toHaveBeenCalled(); expect(runCrestodianMock).not.toHaveBeenCalled(); }); @@ -201,7 +201,7 @@ describe("runCli exit behavior", () => { }); it("bootstraps env proxy before bare Crestodian startup", async () => { - hasEnvHttpProxyConfiguredMock.mockReturnValue(true); + hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true); const stdinTty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY"); const stdoutTty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY"); Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true }); @@ -230,7 +230,7 @@ describe("runCli exit behavior", () => { }); it("bootstraps env proxy before modern onboard Crestodian startup", async () => { - hasEnvHttpProxyConfiguredMock.mockReturnValue(true); + hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true); await runCli(["node", "openclaw", "onboard", "--modern", "--json"]); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index dcb2fc71e89..a2adac4af26 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -84,8 +84,8 @@ function isCommanderParseExit(error: unknown): error is { exitCode: number } { async function ensureCliEnvProxyDispatcher(): Promise { try { - const { hasEnvHttpProxyConfigured } = await import("../infra/net/proxy-env.js"); - if (!hasEnvHttpProxyConfigured("https")) { + const { hasEnvHttpProxyAgentConfigured } = await import("../infra/net/proxy-env.js"); + if (!hasEnvHttpProxyAgentConfigured()) { return; } const { ensureGlobalUndiciEnvProxyDispatcher } = diff --git a/src/infra/net/proxy-env.test.ts b/src/infra/net/proxy-env.test.ts index addbd0b4d8d..875b4b0eeff 100644 --- a/src/infra/net/proxy-env.test.ts +++ b/src/infra/net/proxy-env.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from "vitest"; import { + hasEnvHttpProxyAgentConfigured, hasEnvHttpProxyConfigured, hasProxyEnvConfigured, matchesNoProxy, + resolveEnvHttpProxyAgentOptions, resolveEnvHttpProxyUrl, shouldUseEnvHttpProxyForUrl, } from "./proxy-env.js"; @@ -96,6 +98,55 @@ describe("resolveEnvHttpProxyUrl", () => { }); }); +describe("resolveEnvHttpProxyAgentOptions", () => { + it.each([ + { + name: "maps HTTPS_PROXY to httpsProxy only", + env: { HTTPS_PROXY: "http://https-proxy.test:8443" } as NodeJS.ProcessEnv, + expected: { httpsProxy: "http://https-proxy.test:8443" }, + }, + { + name: "uses HTTP_PROXY as HTTPS fallback", + env: { HTTP_PROXY: "http://http-proxy.test:8080" } as NodeJS.ProcessEnv, + expected: { + httpProxy: "http://http-proxy.test:8080", + httpsProxy: "http://http-proxy.test:8080", + }, + }, + { + name: "uses ALL_PROXY for both protocols", + env: { ALL_PROXY: "socks5://all-proxy.test:1080" } as NodeJS.ProcessEnv, + expected: { + httpProxy: "socks5://all-proxy.test:1080", + httpsProxy: "socks5://all-proxy.test:1080", + }, + }, + { + name: "lets protocol-specific proxy override ALL_PROXY", + env: { + ALL_PROXY: "socks5://all-proxy.test:1080", + HTTP_PROXY: "http://http-proxy.test:8080", + HTTPS_PROXY: "http://https-proxy.test:8443", + } as NodeJS.ProcessEnv, + expected: { + httpProxy: "http://http-proxy.test:8080", + httpsProxy: "http://https-proxy.test:8443", + }, + }, + { + name: "treats empty lower-case all_proxy as authoritative over upper-case ALL_PROXY", + env: { + all_proxy: "", + ALL_PROXY: "socks5://upper-all-proxy.test:1080", + } as NodeJS.ProcessEnv, + expected: undefined, + }, + ])("$name", ({ env, expected }) => { + expect(resolveEnvHttpProxyAgentOptions(env)).toEqual(expected); + expect(hasEnvHttpProxyAgentConfigured(env)).toBe(expected !== undefined); + }); +}); + describe("matchesNoProxy", () => { it.each([ { diff --git a/src/infra/net/proxy-env.ts b/src/infra/net/proxy-env.ts index 283fe60d94a..de96d01b1f8 100644 --- a/src/infra/net/proxy-env.ts +++ b/src/infra/net/proxy-env.ts @@ -25,6 +25,11 @@ function normalizeProxyEnvValue(value: string | undefined): string | null | unde return trimmed.length > 0 ? trimmed : null; } +export type EnvHttpProxyAgentProxyOptions = { + httpProxy?: string; + httpsProxy?: string; +}; + /** * Match undici EnvHttpProxyAgent semantics for env-based HTTP/S proxy selection: * - lower-case vars take precedence over upper-case @@ -54,6 +59,37 @@ export function hasEnvHttpProxyConfigured( return resolveEnvHttpProxyUrl(protocol, env) !== undefined; } +function resolveEnvAllProxyUrl(env: NodeJS.ProcessEnv): string | undefined { + const lowerAllProxy = normalizeProxyEnvValue(env.all_proxy); + const allProxy = + lowerAllProxy !== undefined ? lowerAllProxy : normalizeProxyEnvValue(env.ALL_PROXY); + return allProxy ?? undefined; +} + +/** + * Build explicit options for undici's EnvHttpProxyAgent. + * + * EnvHttpProxyAgent does not read ALL_PROXY itself, but it accepts explicit + * HTTP/HTTPS proxy overrides. Keep this helper separate from the + * HTTP(S)-only URL helpers so SSRF trusted-env proxy gates do not widen. + */ +export function resolveEnvHttpProxyAgentOptions( + env: NodeJS.ProcessEnv = process.env, +): EnvHttpProxyAgentProxyOptions | undefined { + const allProxy = resolveEnvAllProxyUrl(env); + const httpProxy = resolveEnvHttpProxyUrl("http", env) ?? allProxy; + const httpsProxy = resolveEnvHttpProxyUrl("https", env) ?? httpProxy; + const options: EnvHttpProxyAgentProxyOptions = { + ...(httpProxy ? { httpProxy } : {}), + ...(httpsProxy ? { httpsProxy } : {}), + }; + return options.httpProxy || options.httpsProxy ? options : undefined; +} + +export function hasEnvHttpProxyAgentConfigured(env: NodeJS.ProcessEnv = process.env): boolean { + return resolveEnvHttpProxyAgentOptions(env) !== undefined; +} + export function shouldUseEnvHttpProxyForUrl( targetUrl: string, env: NodeJS.ProcessEnv = process.env, diff --git a/src/infra/net/proxy-fetch.test.ts b/src/infra/net/proxy-fetch.test.ts index f058d521d42..92b38ad1302 100644 --- a/src/infra/net/proxy-fetch.test.ts +++ b/src/infra/net/proxy-fetch.test.ts @@ -29,9 +29,9 @@ const { ProxyAgent, EnvHttpProxyAgent, undiciFetch, proxyAgentSpy, envAgentSpy, } class EnvHttpProxyAgent { static lastCreated: EnvHttpProxyAgent | undefined; - constructor() { + constructor(public readonly options?: Record) { EnvHttpProxyAgent.lastCreated = this; - envAgentSpy(); + envAgentSpy(options); } } @@ -159,7 +159,7 @@ describe("resolveProxyFetchFromEnv", () => { HTTPS_PROXY: "http://proxy.test:8080", }); expect(fetchFn).toBeDefined(); - expect(envAgentSpy).toHaveBeenCalled(); + expect(envAgentSpy).toHaveBeenCalledWith({ httpsProxy: "http://proxy.test:8080" }); await fetchFn!("https://api.example.com"); expect(undiciFetch).toHaveBeenCalledWith( @@ -174,7 +174,10 @@ describe("resolveProxyFetchFromEnv", () => { HTTP_PROXY: "http://fallback.test:3128", }); expect(fetchFn).toBeDefined(); - expect(envAgentSpy).toHaveBeenCalled(); + expect(envAgentSpy).toHaveBeenCalledWith({ + httpProxy: "http://fallback.test:3128", + httpsProxy: "http://fallback.test:3128", + }); }); it("returns proxy fetch when lowercase https_proxy is set", () => { @@ -185,7 +188,7 @@ describe("resolveProxyFetchFromEnv", () => { https_proxy: "http://lower.test:1080", }); expect(fetchFn).toBeDefined(); - expect(envAgentSpy).toHaveBeenCalled(); + expect(envAgentSpy).toHaveBeenCalledWith({ httpsProxy: "http://lower.test:1080" }); }); it("returns proxy fetch when lowercase http_proxy is set", () => { @@ -196,7 +199,25 @@ describe("resolveProxyFetchFromEnv", () => { http_proxy: "http://lower-http.test:1080", }); expect(fetchFn).toBeDefined(); - expect(envAgentSpy).toHaveBeenCalled(); + expect(envAgentSpy).toHaveBeenCalledWith({ + httpProxy: "http://lower-http.test:1080", + httpsProxy: "http://lower-http.test:1080", + }); + }); + + it("returns proxy fetch when ALL_PROXY is set", () => { + const fetchFn = resolveProxyFetchFromEnv({ + HTTPS_PROXY: "", + HTTP_PROXY: "", + https_proxy: "", + http_proxy: "", + ALL_PROXY: "socks5://all-proxy.test:1080", + }); + expect(fetchFn).toBeDefined(); + expect(envAgentSpy).toHaveBeenCalledWith({ + httpProxy: "socks5://all-proxy.test:1080", + httpsProxy: "socks5://all-proxy.test:1080", + }); }); it("returns undefined when EnvHttpProxyAgent constructor throws", () => { diff --git a/src/infra/net/proxy-fetch.ts b/src/infra/net/proxy-fetch.ts index 57b1c6004fa..d78b6fe06be 100644 --- a/src/infra/net/proxy-fetch.ts +++ b/src/infra/net/proxy-fetch.ts @@ -1,7 +1,7 @@ import { EnvHttpProxyAgent, ProxyAgent, fetch as undiciFetch } from "undici"; import { logWarn } from "../../logger.js"; import { formatErrorMessage } from "../errors.js"; -import { hasEnvHttpProxyConfigured } from "./proxy-env.js"; +import { resolveEnvHttpProxyAgentOptions } from "./proxy-env.js"; export const PROXY_FETCH_PROXY_URL = Symbol.for("openclaw.proxyFetch.proxyUrl"); type ProxyFetchWithMetadata = typeof fetch & { @@ -46,8 +46,7 @@ export function getProxyUrlFromFetch(fetchImpl?: typeof fetch): string | undefin } /** - * Resolve a proxy-aware fetch from standard environment variables - * (HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy). + * Resolve a proxy-aware fetch from standard environment variables. * Respects NO_PROXY / no_proxy exclusions via undici's EnvHttpProxyAgent. * Returns undefined when no proxy is configured. * Gracefully returns undefined if the proxy URL is malformed. @@ -55,11 +54,12 @@ export function getProxyUrlFromFetch(fetchImpl?: typeof fetch): string | undefin export function resolveProxyFetchFromEnv( env: NodeJS.ProcessEnv = process.env, ): typeof fetch | undefined { - if (!hasEnvHttpProxyConfigured("https", env)) { + const proxyOptions = resolveEnvHttpProxyAgentOptions(env); + if (!proxyOptions) { return undefined; } try { - const agent = new EnvHttpProxyAgent(); + const agent = new EnvHttpProxyAgent(proxyOptions); return ((input: RequestInfo | URL, init?: RequestInit) => undiciFetch(input as string | URL, { ...(init as Record), diff --git a/src/infra/net/undici-global-dispatcher.test.ts b/src/infra/net/undici-global-dispatcher.test.ts index 41092c34e2b..9322799654f 100644 --- a/src/infra/net/undici-global-dispatcher.test.ts +++ b/src/infra/net/undici-global-dispatcher.test.ts @@ -60,7 +60,8 @@ vi.mock("node:net", () => ({ })); vi.mock("./proxy-env.js", () => ({ - hasEnvHttpProxyConfigured: vi.fn(() => false), + hasEnvHttpProxyAgentConfigured: vi.fn(() => false), + resolveEnvHttpProxyAgentOptions: vi.fn(() => undefined), })); vi.mock("../wsl.js", () => ({ @@ -68,7 +69,7 @@ vi.mock("../wsl.js", () => ({ })); import { isWSL2Sync } from "../wsl.js"; -import { hasEnvHttpProxyConfigured } from "./proxy-env.js"; +import { hasEnvHttpProxyAgentConfigured, resolveEnvHttpProxyAgentOptions } from "./proxy-env.js"; let DEFAULT_UNDICI_STREAM_TIMEOUT_MS: typeof import("./undici-global-dispatcher.js").DEFAULT_UNDICI_STREAM_TIMEOUT_MS; let ensureGlobalUndiciEnvProxyDispatcher: typeof import("./undici-global-dispatcher.js").ensureGlobalUndiciEnvProxyDispatcher; let ensureGlobalUndiciStreamTimeouts: typeof import("./undici-global-dispatcher.js").ensureGlobalUndiciStreamTimeouts; @@ -91,7 +92,8 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { resetGlobalUndiciStreamTimeoutsForTests(); setCurrentDispatcher(new Agent()); getDefaultAutoSelectFamily.mockReturnValue(undefined); - vi.mocked(hasEnvHttpProxyConfigured).mockReturnValue(false); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(false); + vi.mocked(resolveEnvHttpProxyAgentOptions).mockReturnValue(undefined); }); it("replaces default Agent dispatcher with extended stream timeouts", () => { @@ -127,6 +129,28 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { }); }); + it("preserves explicit env proxy options when replacing EnvHttpProxyAgent dispatcher", () => { + vi.mocked(resolveEnvHttpProxyAgentOptions).mockReturnValue({ + httpProxy: "socks5://proxy.test:1080", + httpsProxy: "socks5://proxy.test:1080", + }); + setCurrentDispatcher(new EnvHttpProxyAgent()); + + ensureGlobalUndiciStreamTimeouts(); + + expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); + const next = getCurrentDispatcher() as { options?: Record }; + expect(next).toBeInstanceOf(EnvHttpProxyAgent); + expect(next.options).toEqual( + expect.objectContaining({ + httpProxy: "socks5://proxy.test:1080", + httpsProxy: "socks5://proxy.test:1080", + bodyTimeout: DEFAULT_UNDICI_STREAM_TIMEOUT_MS, + headersTimeout: DEFAULT_UNDICI_STREAM_TIMEOUT_MS, + }), + ); + }); + it("records timeout bridge but does not override unsupported custom proxy dispatcher types", () => { setCurrentDispatcher(new ProxyAgent("http://proxy.test:8080")); @@ -201,11 +225,12 @@ describe("ensureGlobalUndiciEnvProxyDispatcher", () => { vi.clearAllMocks(); resetGlobalUndiciStreamTimeoutsForTests(); setCurrentDispatcher(new Agent()); - vi.mocked(hasEnvHttpProxyConfigured).mockReturnValue(false); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(false); + vi.mocked(resolveEnvHttpProxyAgentOptions).mockReturnValue(undefined); }); it("installs EnvHttpProxyAgent when env HTTP proxy is configured on a default Agent", () => { - vi.mocked(hasEnvHttpProxyConfigured).mockReturnValue(true); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); ensureGlobalUndiciEnvProxyDispatcher(); @@ -213,8 +238,26 @@ describe("ensureGlobalUndiciEnvProxyDispatcher", () => { expect(getCurrentDispatcher()).toBeInstanceOf(EnvHttpProxyAgent); }); + it("installs EnvHttpProxyAgent with explicit ALL_PROXY fallback options", () => { + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); + vi.mocked(resolveEnvHttpProxyAgentOptions).mockReturnValue({ + httpProxy: "socks5://proxy.test:1080", + httpsProxy: "socks5://proxy.test:1080", + }); + + ensureGlobalUndiciEnvProxyDispatcher(); + + expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); + const next = getCurrentDispatcher() as { options?: Record }; + expect(next).toBeInstanceOf(EnvHttpProxyAgent); + expect(next.options).toEqual({ + httpProxy: "socks5://proxy.test:1080", + httpsProxy: "socks5://proxy.test:1080", + }); + }); + it("does not override unsupported custom proxy dispatcher types", () => { - vi.mocked(hasEnvHttpProxyConfigured).mockReturnValue(true); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); setCurrentDispatcher(new ProxyAgent("http://proxy.test:8080")); ensureGlobalUndiciEnvProxyDispatcher(); @@ -223,7 +266,7 @@ describe("ensureGlobalUndiciEnvProxyDispatcher", () => { }); it("retries proxy bootstrap after an unsupported dispatcher later becomes a default Agent", () => { - vi.mocked(hasEnvHttpProxyConfigured).mockReturnValue(true); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); setCurrentDispatcher(new ProxyAgent("http://proxy.test:8080")); ensureGlobalUndiciEnvProxyDispatcher(); @@ -237,7 +280,7 @@ describe("ensureGlobalUndiciEnvProxyDispatcher", () => { }); it("is idempotent after proxy bootstrap succeeds", () => { - vi.mocked(hasEnvHttpProxyConfigured).mockReturnValue(true); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); ensureGlobalUndiciEnvProxyDispatcher(); ensureGlobalUndiciEnvProxyDispatcher(); @@ -246,7 +289,7 @@ describe("ensureGlobalUndiciEnvProxyDispatcher", () => { }); it("reinstalls env proxy if an external change later reverts the dispatcher to Agent", () => { - vi.mocked(hasEnvHttpProxyConfigured).mockReturnValue(true); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); ensureGlobalUndiciEnvProxyDispatcher(); expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); diff --git a/src/infra/net/undici-global-dispatcher.ts b/src/infra/net/undici-global-dispatcher.ts index 0bef2b84511..a37afa0532b 100644 --- a/src/infra/net/undici-global-dispatcher.ts +++ b/src/infra/net/undici-global-dispatcher.ts @@ -1,7 +1,7 @@ import * as net from "node:net"; import { Agent, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici"; import { isWSL2Sync } from "../wsl.js"; -import { hasEnvHttpProxyConfigured } from "./proxy-env.js"; +import { hasEnvHttpProxyAgentConfigured, resolveEnvHttpProxyAgentOptions } from "./proxy-env.js"; export const DEFAULT_UNDICI_STREAM_TIMEOUT_MS = 30 * 60 * 1000; @@ -89,7 +89,7 @@ function resolveCurrentDispatcherKind(): DispatcherKind | null { } export function ensureGlobalUndiciEnvProxyDispatcher(): void { - const shouldUseEnvProxy = hasEnvHttpProxyConfigured("https"); + const shouldUseEnvProxy = hasEnvHttpProxyAgentConfigured(); if (!shouldUseEnvProxy) { return; } @@ -108,7 +108,7 @@ export function ensureGlobalUndiciEnvProxyDispatcher(): void { return; } try { - setGlobalDispatcher(new EnvHttpProxyAgent()); + setGlobalDispatcher(new EnvHttpProxyAgent(resolveEnvHttpProxyAgentOptions())); lastAppliedProxyBootstrap = true; } catch { // Best-effort bootstrap only. @@ -137,6 +137,7 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }): try { if (kind === "env-proxy") { const proxyOptions = { + ...resolveEnvHttpProxyAgentOptions(), bodyTimeout: timeoutMs, headersTimeout: timeoutMs, ...(connect ? { connect } : {}),