Skip to content

Fix/global proxy dispatcher#516

Open
BingqingLyu wants to merge 2 commits intomainfrom
fork-pr-43919-fix-global-proxy-dispatcher
Open

Fix/global proxy dispatcher#516
BingqingLyu wants to merge 2 commits intomainfrom
fork-pr-43919-fix-global-proxy-dispatcher

Conversation

@BingqingLyu
Copy link
Copy Markdown
Owner

@BingqingLyu BingqingLyu commented Apr 27, 2026

Summary

  • Problem: LLM inference requests time out when the gateway runs behind a proxy (HTTP_PROXY / HTTPS_PROXY / ALL_PROXY), because Node.js 22+'s globalThis.fetch (undici) does not read proxy env vars by default, and the upstream streamSimple in @mariozechner/pi-ai does not accept a dispatcher parameter.
  • Why it matters: Users in corporate firewalls, and other restricted networks cannot use OpenClaw for LLM inference at all — the only working path (curl -x) proves the proxy itself is fine.
  • What changed: Added applyGlobalProxyDispatcher() in src/infra/net/global-proxy.ts that calls setGlobalDispatcher(new EnvHttpProxyAgent()) early in gateway startup (after dotenv loading), making all globalThis.fetch calls proxy-aware. Added ALL_PROXY / socks5:// / socks5h:// normalization in src/infra/net/proxy-env.ts. 17 unit tests.
  • What did NOT change (scope boundary): No modifications to existing Telegram, WhatsApp, media-understanding, or web-tools proxy log.

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • When HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY env vars are set, LLM inference requests now route through the proxy instead of timing out.
  • [net/global-proxy] global undici dispatcher set to EnvHttpProxyAgent (via ...) log line appears at startup when proxy is active.
  • ALL_PROXY with socks5:// or socks5h:// is rewritten to http:// for undici compatibility (most local proxy tools like Clash/V2Ray accept HTTP CONNECT on the same port).
  • NO_PROXY / no_proxy exclusions are respected — localhost services (Ollama, browser control, canvas host) are not affected.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? Yes — existing globalThis.fetch calls are now routed through the user-configured proxy when env vars are set. No new outbound calls are introduced.
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: The proxy dispatcher only activates when the user explicitly sets proxy env vars. EnvHttpProxyAgent respects NO_PROXY exclusions, so internal/localhost traffic is unaffected. If EnvHttpProxyAgent construction fails (e.g. malformed URL), the error is logged and the gateway continues with direct connections.

Repro + Verification

Environment

  • OS: macOS (Darwin 25.2.0), also affects Linux
  • Runtime/container: Node.js 22+
  • Model/provider: Any (Google Gemini, Anthropic, OpenAI-compatible)
  • Integration/channel (if any): All channels — the fix is at the global fetch layer
  • Relevant config (redacted): HTTP_PROXY=http://127.0.0.1:7897 HTTPS_PROXY=http://127.0.0.1:7897 ALL_PROXY=socks5://127.0.0.1:7897

Steps

  1. Be in a network where direct connections to LLM APIs are blocked (common in China, corporate firewalls)
  2. Start gateway: env HTTP_PROXY="http://127.0.0.1:7897" HTTPS_PROXY="http://127.0.0.1:7897" openclaw gateway
  3. Send any message that triggers LLM inference

Expected

  • LLM inference requests succeed through the proxy
  • [net/global-proxy] global undici dispatcher set to EnvHttpProxyAgent (via HTTP_PROXY) in startup log

Actual (before fix)

  • Repeated LLM request timed out errors, even though curl -x http://127.0.0.1:7897 https://generativelanguage.googleapis.com/... succeeds

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Unit tests (17 cases, all passing):

✓ sets global dispatcher when HTTPS_PROXY is set
✓ sets global dispatcher when HTTP_PROXY is set
✓ sets global dispatcher when ALL_PROXY is set
✓ rewrites socks5:// ALL_PROXY to http:// for EnvHttpProxyAgent compatibility
✓ rewrites socks5h:// ALL_PROXY to http:// for EnvHttpProxyAgent compatibility
✓ passes http:// ALL_PROXY as-is
✓ passes all_proxy (lowercase) as explicit httpProxy/httpsProxy
✓ prefers lowercase all_proxy over uppercase ALL_PROXY
✓ does not pass explicit options when HTTP_PROXY is also set alongside ALL_PROXY
✓ does not pass explicit options when HTTPS_PROXY is set (no ALL_PROXY fallback needed)
✓ sets global dispatcher when lowercase proxy vars are set
✓ is a no-op when no proxy env var is set
✓ only applies once even if called multiple times
✓ skips if a proxy-like dispatcher is already installed
✓ does not skip if existing dispatcher is not proxy-like
✓ handles EnvHttpProxyAgent constructor failure gracefully

Regression tests pass with zero regressions:

  • src/infra/net/proxy-fetch.test.ts — 7/7 passed
  • src/infra/net/fetch-guard.ssrf.test.ts — 9/9 passed

Startup log confirming proxy activation:

2026-03-13T14:56:08.218+08:00 [net/global-proxy] global undici dispatcher set to EnvHttpProxyAgent (via HTTP_PROXY)

Human Verification

  • Verified scenarios:
    • Built and installed locally via npm link (OpenClaw 2026.3.13, commit 2b46c4c)
    • Started gateway with env OPENCLAW_DISABLE_BONJOUR=1 HTTP_PROXY="http://127.0.0.1:7897" HTTPS_PROXY="http://127.0.0.1:7897" ALL_PROXY="socks5://127.0.0.1:7897" openclaw gateway run --force
    • Confirmed [net/global-proxy] global undici dispatcher set to EnvHttpProxyAgent (via HTTP_PROXY) in startup log
    • Gateway started successfully, all channels connected, subagent dispatch unaffected
    • Localhost services (browser control on :18791, canvas host) remain accessible without proxy interference
  • Edge cases checked:
    • socks5h:// protocol variant (was broken by original regex, now fixed)
    • ALL_PROXY-only configuration (no HTTP_PROXY/HTTPS_PROXY set)
    • Idempotent re-invocation (safe to call multiple times)
    • Cooperative skip when another proxy dispatcher is already installed
    • Graceful failure on malformed proxy URL
  • What I did not verify:
    • Windows-specific behavior (lowercase vs uppercase env var precedence)
    • SOCKS-only proxy endpoints that do not accept HTTP CONNECT on the same port

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No — uses existing standard proxy env vars (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY)
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Remove the applyGlobalProxyDispatcher() call in src/gateway/server.impl.ts (single line). Or simply unset proxy env vars — the function is a no-op without them.
  • Files/config to restore: src/gateway/server.impl.ts (remove import + call)
  • Known bad symptoms reviewers should watch for: If a SOCKS-only proxy does not accept HTTP CONNECT, the rewrite from socks5:// to http:// will cause connection failures. Users would see fetch errors instead of timeouts.

Risks and Mitigations

  • Risk: SOCKS5-to-HTTP rewrite assumes the proxy accepts HTTP CONNECT on the same port.
    • Mitigation: This is true for all major local proxy tools (Clash, V2Ray, Shadowsocks, Surge). For truly SOCKS-only endpoints, the connection will fail at connect time with a clear error, which is still better than silently timing out.
  • Risk: Global dispatcher affects all globalThis.fetch calls process-wide.
    • Mitigation: This is intentional — the whole point is to cover streamSimple which only uses globalThis.fetch. EnvHttpProxyAgent respects NO_PROXY so internal traffic is excluded. The function is cooperative (skips if a proxy dispatcher is already installed) and idempotent.

Comparison with existing open PRs

PR Approach Limitations
openclaw#33990 ProxyAgent in server-startup.ts Does not respect NO_PROXY; modifies Telegram code
openclaw#42320 EnvHttpProxyAgent in undici-global-dispatcher.ts Only covers embedded runner path, not startup/heartbeat/web-search
openclaw#41455 Similar to openclaw#42320 Excludes ALL_PROXY; only covers embedded runner

Files changed

  • src/infra/net/global-proxy.ts — New module: applyGlobalProxyDispatcher() + test reset helper
  • src/infra/net/global-proxy.test.ts — 17 unit tests
  • src/infra/net/proxy-env.ts — Added resolveAllProxyFallbackOptions(), SOCKS protocol normalization (builds on resolveEnvHttpProxyUrl / hasEnvHttpProxyConfigured from Fix env proxy bootstrap for model traffic openclaw/openclaw#43248)
  • src/gateway/server.impl.ts — Import and call applyGlobalProxyDispatcher() after readConfigFileSnapshot()

Relationship with openclaw#43248

openclaw#43248 (merged 2026-03-11) added ensureGlobalUndiciEnvProxyDispatcher() in undici-global-dispatcher.ts, called inside the embedded runner's runEmbeddedAttempt(). This PR complements it with two things openclaw#43248 does not cover:

openclaw#43248 (merged) This PR
Injection point runEmbeddedAttempt() — per-run, embedded runner only startGatewayServer() — once at gateway startup, covers all fetch paths
Coverage Model traffic via embedded runner All globalThis.fetch: LLM inference, heartbeat, web search, usage tracking, etc.
ALL_PROXY support No — explicitly excluded Yes — resolveAllProxyFallbackOptions() bridges undici's EnvHttpProxyAgent gap
SOCKS protocol No Yes — socks5://, socks5h://, socks4:// rewritten to http://
dotenv timing N/A (runs after config is loaded) Explicitly placed after readConfigFileSnapshot() so .env-defined proxy vars are visible

This PR reuses the resolveEnvHttpProxyUrl and hasEnvHttpProxyConfigured helpers that openclaw#43248 added to proxy-env.ts, and adds resolveAllProxyFallbackOptions() on top for the ALL_PROXY/SOCKS gap.

RickyTong1 and others added 2 commits March 18, 2026 17:37
…d loopback bypass

- Bootstrap undici EnvHttpProxyAgent at gateway startup when proxy env
  vars are detected (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY)
- Normalize socks4/4a/4h/5/5h:// URLs to http:// for undici compat
- Route ALL_PROXY fallback through both httpProxy and httpsProxy
- Ensure loopback addresses (localhost, 127.0.0.1, [::1]) are always
  bypassed via NO_PROXY merging; preserve NO_PROXY=* wildcard
- Success-latch prevents redundant re-installation while allowing
  retry after no-op or constructor failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Add global HTTP proxy support via environment variables HTTP_PROXY/HTTPS_PROXY environment variables ignored by undici fetch

2 participants