Skip to content

Commit dc85958

Browse files
committed
fix(gateway): honor all_proxy in env dispatcher
1 parent fd6c9fc commit dc85958

11 files changed

Lines changed: 188 additions & 31 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
3838
- 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 <name> plugins install ...` no longer writes into the default profile. Fixes #69960; carries forward #69971. Thanks @FrancisLyman and @Sanjays2402.
3939
- 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.
4040
- 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.
41+
- 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.
4142
- 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.
4243
- 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.
4344
- Nodes/CLI: add `openclaw nodes remove --node <id|name|ip>` and `node.pair.remove` so stale gateway-owned node pairing records can be cleaned without hand-editing state files.

docs/nodes/audio.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,10 @@ Provider-based audio transcription honors standard outbound proxy env vars:
171171

172172
- `HTTPS_PROXY`
173173
- `HTTP_PROXY`
174+
- `ALL_PROXY`
174175
- `https_proxy`
175176
- `http_proxy`
177+
- `all_proxy`
176178

177179
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.
178180

docs/nodes/media-understanding.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,10 @@ When provider-based **audio** and **video** media understanding is enabled, Open
220220

221221
- `HTTPS_PROXY`
222222
- `HTTP_PROXY`
223+
- `ALL_PROXY`
223224
- `https_proxy`
224225
- `http_proxy`
226+
- `all_proxy`
225227

226228
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.
227229

src/cli/run-main.exit.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
2020
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
2121
const registerSubCliByNameMock = vi.hoisted(() => vi.fn());
2222
const restoreTerminalStateMock = vi.hoisted(() => vi.fn());
23-
const hasEnvHttpProxyConfiguredMock = vi.hoisted(() => vi.fn(() => false));
23+
const hasEnvHttpProxyAgentConfiguredMock = vi.hoisted(() => vi.fn(() => false));
2424
const ensureGlobalUndiciEnvProxyDispatcherMock = vi.hoisted(() => vi.fn());
2525
const runCrestodianMock = vi.hoisted(() => vi.fn(async () => {}));
2626
const progressDoneMock = vi.hoisted(() => vi.fn());
@@ -106,7 +106,7 @@ vi.mock("../terminal/restore.js", () => ({
106106
}));
107107

108108
vi.mock("../infra/net/proxy-env.js", () => ({
109-
hasEnvHttpProxyConfigured: hasEnvHttpProxyConfiguredMock,
109+
hasEnvHttpProxyAgentConfigured: hasEnvHttpProxyAgentConfiguredMock,
110110
}));
111111

112112
vi.mock("../infra/net/undici-global-dispatcher.js", () => ({
@@ -127,7 +127,7 @@ describe("runCli exit behavior", () => {
127127
hasMemoryRuntimeMock.mockReturnValue(false);
128128
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
129129
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
130-
hasEnvHttpProxyConfiguredMock.mockReturnValue(false);
130+
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false);
131131
getProgramContextMock.mockReturnValue(null);
132132
delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
133133
});
@@ -178,7 +178,7 @@ describe("runCli exit behavior", () => {
178178
await runCli(["node", "openclaw", "--help"]);
179179

180180
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
181-
expect(hasEnvHttpProxyConfiguredMock).not.toHaveBeenCalled();
181+
expect(hasEnvHttpProxyAgentConfiguredMock).not.toHaveBeenCalled();
182182
expect(ensureGlobalUndiciEnvProxyDispatcherMock).not.toHaveBeenCalled();
183183
expect(runCrestodianMock).not.toHaveBeenCalled();
184184
});
@@ -201,7 +201,7 @@ describe("runCli exit behavior", () => {
201201
});
202202

203203
it("bootstraps env proxy before bare Crestodian startup", async () => {
204-
hasEnvHttpProxyConfiguredMock.mockReturnValue(true);
204+
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true);
205205
const stdinTty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
206206
const stdoutTty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
207207
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true });
@@ -230,7 +230,7 @@ describe("runCli exit behavior", () => {
230230
});
231231

232232
it("bootstraps env proxy before modern onboard Crestodian startup", async () => {
233-
hasEnvHttpProxyConfiguredMock.mockReturnValue(true);
233+
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true);
234234

235235
await runCli(["node", "openclaw", "onboard", "--modern", "--json"]);
236236

src/cli/run-main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ function isCommanderParseExit(error: unknown): error is { exitCode: number } {
8484

8585
async function ensureCliEnvProxyDispatcher(): Promise<void> {
8686
try {
87-
const { hasEnvHttpProxyConfigured } = await import("../infra/net/proxy-env.js");
88-
if (!hasEnvHttpProxyConfigured("https")) {
87+
const { hasEnvHttpProxyAgentConfigured } = await import("../infra/net/proxy-env.js");
88+
if (!hasEnvHttpProxyAgentConfigured()) {
8989
return;
9090
}
9191
const { ensureGlobalUndiciEnvProxyDispatcher } =

src/infra/net/proxy-env.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { describe, expect, it } from "vitest";
22
import {
3+
hasEnvHttpProxyAgentConfigured,
34
hasEnvHttpProxyConfigured,
45
hasProxyEnvConfigured,
56
matchesNoProxy,
7+
resolveEnvHttpProxyAgentOptions,
68
resolveEnvHttpProxyUrl,
79
shouldUseEnvHttpProxyForUrl,
810
} from "./proxy-env.js";
@@ -96,6 +98,55 @@ describe("resolveEnvHttpProxyUrl", () => {
9698
});
9799
});
98100

101+
describe("resolveEnvHttpProxyAgentOptions", () => {
102+
it.each([
103+
{
104+
name: "maps HTTPS_PROXY to httpsProxy only",
105+
env: { HTTPS_PROXY: "http://https-proxy.test:8443" } as NodeJS.ProcessEnv,
106+
expected: { httpsProxy: "http://https-proxy.test:8443" },
107+
},
108+
{
109+
name: "uses HTTP_PROXY as HTTPS fallback",
110+
env: { HTTP_PROXY: "http://http-proxy.test:8080" } as NodeJS.ProcessEnv,
111+
expected: {
112+
httpProxy: "http://http-proxy.test:8080",
113+
httpsProxy: "http://http-proxy.test:8080",
114+
},
115+
},
116+
{
117+
name: "uses ALL_PROXY for both protocols",
118+
env: { ALL_PROXY: "socks5://all-proxy.test:1080" } as NodeJS.ProcessEnv,
119+
expected: {
120+
httpProxy: "socks5://all-proxy.test:1080",
121+
httpsProxy: "socks5://all-proxy.test:1080",
122+
},
123+
},
124+
{
125+
name: "lets protocol-specific proxy override ALL_PROXY",
126+
env: {
127+
ALL_PROXY: "socks5://all-proxy.test:1080",
128+
HTTP_PROXY: "http://http-proxy.test:8080",
129+
HTTPS_PROXY: "http://https-proxy.test:8443",
130+
} as NodeJS.ProcessEnv,
131+
expected: {
132+
httpProxy: "http://http-proxy.test:8080",
133+
httpsProxy: "http://https-proxy.test:8443",
134+
},
135+
},
136+
{
137+
name: "treats empty lower-case all_proxy as authoritative over upper-case ALL_PROXY",
138+
env: {
139+
all_proxy: "",
140+
ALL_PROXY: "socks5://upper-all-proxy.test:1080",
141+
} as NodeJS.ProcessEnv,
142+
expected: undefined,
143+
},
144+
])("$name", ({ env, expected }) => {
145+
expect(resolveEnvHttpProxyAgentOptions(env)).toEqual(expected);
146+
expect(hasEnvHttpProxyAgentConfigured(env)).toBe(expected !== undefined);
147+
});
148+
});
149+
99150
describe("matchesNoProxy", () => {
100151
it.each([
101152
{

src/infra/net/proxy-env.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ function normalizeProxyEnvValue(value: string | undefined): string | null | unde
2525
return trimmed.length > 0 ? trimmed : null;
2626
}
2727

28+
export type EnvHttpProxyAgentProxyOptions = {
29+
httpProxy?: string;
30+
httpsProxy?: string;
31+
};
32+
2833
/**
2934
* Match undici EnvHttpProxyAgent semantics for env-based HTTP/S proxy selection:
3035
* - lower-case vars take precedence over upper-case
@@ -54,6 +59,37 @@ export function hasEnvHttpProxyConfigured(
5459
return resolveEnvHttpProxyUrl(protocol, env) !== undefined;
5560
}
5661

62+
function resolveEnvAllProxyUrl(env: NodeJS.ProcessEnv): string | undefined {
63+
const lowerAllProxy = normalizeProxyEnvValue(env.all_proxy);
64+
const allProxy =
65+
lowerAllProxy !== undefined ? lowerAllProxy : normalizeProxyEnvValue(env.ALL_PROXY);
66+
return allProxy ?? undefined;
67+
}
68+
69+
/**
70+
* Build explicit options for undici's EnvHttpProxyAgent.
71+
*
72+
* EnvHttpProxyAgent does not read ALL_PROXY itself, but it accepts explicit
73+
* HTTP/HTTPS proxy overrides. Keep this helper separate from the
74+
* HTTP(S)-only URL helpers so SSRF trusted-env proxy gates do not widen.
75+
*/
76+
export function resolveEnvHttpProxyAgentOptions(
77+
env: NodeJS.ProcessEnv = process.env,
78+
): EnvHttpProxyAgentProxyOptions | undefined {
79+
const allProxy = resolveEnvAllProxyUrl(env);
80+
const httpProxy = resolveEnvHttpProxyUrl("http", env) ?? allProxy;
81+
const httpsProxy = resolveEnvHttpProxyUrl("https", env) ?? httpProxy;
82+
const options: EnvHttpProxyAgentProxyOptions = {
83+
...(httpProxy ? { httpProxy } : {}),
84+
...(httpsProxy ? { httpsProxy } : {}),
85+
};
86+
return options.httpProxy || options.httpsProxy ? options : undefined;
87+
}
88+
89+
export function hasEnvHttpProxyAgentConfigured(env: NodeJS.ProcessEnv = process.env): boolean {
90+
return resolveEnvHttpProxyAgentOptions(env) !== undefined;
91+
}
92+
5793
export function shouldUseEnvHttpProxyForUrl(
5894
targetUrl: string,
5995
env: NodeJS.ProcessEnv = process.env,

src/infra/net/proxy-fetch.test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ const { ProxyAgent, EnvHttpProxyAgent, undiciFetch, proxyAgentSpy, envAgentSpy,
2929
}
3030
class EnvHttpProxyAgent {
3131
static lastCreated: EnvHttpProxyAgent | undefined;
32-
constructor() {
32+
constructor(public readonly options?: Record<string, unknown>) {
3333
EnvHttpProxyAgent.lastCreated = this;
34-
envAgentSpy();
34+
envAgentSpy(options);
3535
}
3636
}
3737

@@ -159,7 +159,7 @@ describe("resolveProxyFetchFromEnv", () => {
159159
HTTPS_PROXY: "http://proxy.test:8080",
160160
});
161161
expect(fetchFn).toBeDefined();
162-
expect(envAgentSpy).toHaveBeenCalled();
162+
expect(envAgentSpy).toHaveBeenCalledWith({ httpsProxy: "http://proxy.test:8080" });
163163

164164
await fetchFn!("https://api.example.com");
165165
expect(undiciFetch).toHaveBeenCalledWith(
@@ -174,7 +174,10 @@ describe("resolveProxyFetchFromEnv", () => {
174174
HTTP_PROXY: "http://fallback.test:3128",
175175
});
176176
expect(fetchFn).toBeDefined();
177-
expect(envAgentSpy).toHaveBeenCalled();
177+
expect(envAgentSpy).toHaveBeenCalledWith({
178+
httpProxy: "http://fallback.test:3128",
179+
httpsProxy: "http://fallback.test:3128",
180+
});
178181
});
179182

180183
it("returns proxy fetch when lowercase https_proxy is set", () => {
@@ -185,7 +188,7 @@ describe("resolveProxyFetchFromEnv", () => {
185188
https_proxy: "http://lower.test:1080",
186189
});
187190
expect(fetchFn).toBeDefined();
188-
expect(envAgentSpy).toHaveBeenCalled();
191+
expect(envAgentSpy).toHaveBeenCalledWith({ httpsProxy: "http://lower.test:1080" });
189192
});
190193

191194
it("returns proxy fetch when lowercase http_proxy is set", () => {
@@ -196,7 +199,25 @@ describe("resolveProxyFetchFromEnv", () => {
196199
http_proxy: "http://lower-http.test:1080",
197200
});
198201
expect(fetchFn).toBeDefined();
199-
expect(envAgentSpy).toHaveBeenCalled();
202+
expect(envAgentSpy).toHaveBeenCalledWith({
203+
httpProxy: "http://lower-http.test:1080",
204+
httpsProxy: "http://lower-http.test:1080",
205+
});
206+
});
207+
208+
it("returns proxy fetch when ALL_PROXY is set", () => {
209+
const fetchFn = resolveProxyFetchFromEnv({
210+
HTTPS_PROXY: "",
211+
HTTP_PROXY: "",
212+
https_proxy: "",
213+
http_proxy: "",
214+
ALL_PROXY: "socks5://all-proxy.test:1080",
215+
});
216+
expect(fetchFn).toBeDefined();
217+
expect(envAgentSpy).toHaveBeenCalledWith({
218+
httpProxy: "socks5://all-proxy.test:1080",
219+
httpsProxy: "socks5://all-proxy.test:1080",
220+
});
200221
});
201222

202223
it("returns undefined when EnvHttpProxyAgent constructor throws", () => {

src/infra/net/proxy-fetch.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EnvHttpProxyAgent, ProxyAgent, fetch as undiciFetch } from "undici";
22
import { logWarn } from "../../logger.js";
33
import { formatErrorMessage } from "../errors.js";
4-
import { hasEnvHttpProxyConfigured } from "./proxy-env.js";
4+
import { resolveEnvHttpProxyAgentOptions } from "./proxy-env.js";
55

66
export const PROXY_FETCH_PROXY_URL = Symbol.for("openclaw.proxyFetch.proxyUrl");
77
type ProxyFetchWithMetadata = typeof fetch & {
@@ -46,20 +46,20 @@ export function getProxyUrlFromFetch(fetchImpl?: typeof fetch): string | undefin
4646
}
4747

4848
/**
49-
* Resolve a proxy-aware fetch from standard environment variables
50-
* (HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy).
49+
* Resolve a proxy-aware fetch from standard environment variables.
5150
* Respects NO_PROXY / no_proxy exclusions via undici's EnvHttpProxyAgent.
5251
* Returns undefined when no proxy is configured.
5352
* Gracefully returns undefined if the proxy URL is malformed.
5453
*/
5554
export function resolveProxyFetchFromEnv(
5655
env: NodeJS.ProcessEnv = process.env,
5756
): typeof fetch | undefined {
58-
if (!hasEnvHttpProxyConfigured("https", env)) {
57+
const proxyOptions = resolveEnvHttpProxyAgentOptions(env);
58+
if (!proxyOptions) {
5959
return undefined;
6060
}
6161
try {
62-
const agent = new EnvHttpProxyAgent();
62+
const agent = new EnvHttpProxyAgent(proxyOptions);
6363
return ((input: RequestInfo | URL, init?: RequestInit) =>
6464
undiciFetch(input as string | URL, {
6565
...(init as Record<string, unknown>),

0 commit comments

Comments
 (0)