Skip to content

fix(client): bypass environment proxies for loopback service traffic - #60472

Open
Hona wants to merge 1 commit into
devfrom
loopback-proxy-bypass
Open

Hona wants to merge 1 commit into
devfrom
loopback-proxy-bypass

Conversation

@Hona

@Hona Hona commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Issue for this PR

Fixes #52054
Related: #51247
Supersedes #60441, #57030, #57744

Type of change

  • Bug fix

What does this PR do?

Exempts loopback traffic from HTTP(S)_PROXY / ALL_PROXY at both the transport boundary (@opencode/client, @opencode/core) and process startup (@opencode/cli, @opencode/desktop), stopping /api/event SSE stalls, false unresponsive-service kills, and Basic-auth password leaks to forward proxies.

flowchart LR
  CLI["CLI / TUI / SDK"]
  Proxy["HTTP(S)_PROXY"]
  Svc["127.0.0.1 / [::1] / localhost"]
  Shell["Session bash tool"]

  CLI -- "proxy: false + merged NO_PROXY/no_proxy" --> Svc
  CLI -. "remote egress only" .-> Proxy
  CLI -- "unmutated caller NO_PROXY" --> Shell
Loading
  • Transport-level bypass (@opencode/client, @opencode/core): service-probe and OpenCode.make wrap default fetch with Bun's { proxy: false } for localhost, *.localhost, 127.0.0.0/8 (e.g. 127.0.0.2), and [::1], protecting standalone SDK callers and early probes before any CLI entrypoint runs; WebSocketConstructor.proxy now matches bracketed IPv6 [::1], 127.0.0.0/8, and *.localhost.
  • Unified NO_PROXY / no_proxy merge (@opencode/cli, @opencode/desktop): Bun and Node's EnvHttpProxyAgent read lowercase no_proxy alone whenever set while WebSocketConstructor reads uppercase NO_PROXY first, so both spellings receive one case-insensitively deduplicated list (corp.internal,127.0.0.1,localhost,::1).
  • Wildcard * preservation: Keeps NO_PROXY=* intact as "*" instead of "*,127.0.0.1,...", which otherwise breaks Node/Electron's EnvHttpProxyAgent exact noProxy === "*" check under http.setGlobalProxyFromEnv().
  • Saved service env & Desktop startup ordering: Applies the merge inside ServiceConfig.processEnv and after Object.assign(process.env, ...) in server-process.ts so opencode service set env no_proxy ... cannot recapture loopback; runs ensureLoopbackNoProxy() before Desktop's startSidecarProbe() and BackgroundService.connect, dropping the duplicate loadProxyEnvironment call.
  • Unpolluted session tool shells: Env.session() restores the caller's pre-startup NO_PROXY / no_proxy snapshot so bash tool subprocesses do not inherit synthetic loopback exemptions.
type Fetch = typeof globalThis.fetch

export const loopbackAwareFetch: Fetch = Object.assign(
  (input: Parameters<Fetch>[0], init?: Parameters<Fetch>[1]) => {
    const url = input instanceof Request ? input.url : String(input)
    return globalThis.fetch(input, isLoopbackUrl(url) ? Object.assign({}, init, { proxy: false as const }) : init)
  },
  { preconnect: () => undefined },
)

How did you verify your code works?

Unit and integration coverage across @opencode/client, @opencode/cli, @opencode/core, and @opencode/desktop against a recording 502 proxy with HTTP(S)_PROXY / ALL_PROXY set and NO_PROXY empty or split.

Visual

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 72" width="240" height="72" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
  <style>
    .pkt { animation: bypass 2.2s linear infinite; }
    @keyframes bypass {
      0% { transform: translate(-62px, -12px); opacity: 0; }
      12% { transform: translate(-47px, -6.5px); opacity: 1; }
      25% { transform: translate(-31px, -3px); opacity: 1; }
      50% { transform: translate(0, 0); opacity: 1; }
      75% { transform: translate(31px, -3px); opacity: 1; }
      88% { transform: translate(47px, -6.5px); opacity: 1; }
      100% { transform: translate(62px, -12px); opacity: 0; }
    }
    @media (prefers-reduced-motion: reduce) {
      .pkt { animation: none; }
    }
  </style>
  <rect x="14" y="24" width="44" height="24" rx="3" opacity=".85"/>
  <path d="M24 33l4 3-4 3M31 39h7" stroke-width="1.5" opacity=".75"/>
  <rect x="98" y="8" width="44" height="20" rx="3" stroke-dasharray="3 3" opacity=".32"/>
  <path d="M116 15l8 6M124 15l-8 6" stroke-width="1.5" opacity=".4"/>
  <path d="M58 30L96 18" stroke-dasharray="2 4" opacity=".22"/>
  <rect x="182" y="24" width="44" height="24" rx="3" opacity=".85"/>
  <circle cx="192" cy="36" r="2" fill="currentColor" stroke="none" opacity=".75"/>
  <path d="M199 36h17" stroke-width="1.5" opacity=".75"/>
  <path d="M58 40 Q 120 64 182 40" opacity=".6"/>
  <path d="M176.5 38.5L182 40l-2.5 5" opacity=".6"/>
  <circle class="pkt" cx="120" cy="52" r="3.2" fill="currentColor" stroke="none"/>
</svg>

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Fixes #52054

Related: #49796, #47041, #54523, #51247

Public API: @opencode/client OpenCode.make bypasses env proxies for loopback URLs by default; CLI/Desktop add loopback hosts to NO_PROXY/no_proxy (backwards compatible)

Copilot AI balanced review requested due to automatic review settings October 30, 2026 04:37
@Hona
Hona requested a review from Brendonovich as a code owner October 30, 2026 04:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

Co-authored-by: Aiden Cline <[email protected]>

Co-authored-by: r266-tech <[email protected]>

Co-authored-by: pj.reid <[email protected]>
@Hona
Hona force-pushed the loopback-proxy-bypass branch from 3ee86c9 to f3be23e Compare October 30, 2026 04:40

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proxy fix works. I ran the PR's own end-to-end tests on base and on this branch. On base, the CLI's /api/info request and the client's discovery/API/SSE requests to a loopback server all went through a stub HTTP_PROXY (502). With this PR they connect directly. The new and nearby tests in cli, client, core and desktop pass. I did not test the desktop app or Windows.

Public API: in @opencode/client, OpenCode.make now uses a fetch that sets proxy: false for loopback URLs when the caller doesn't pass its own fetch. The CLI and Desktop now add 127.0.0.1,localhost,::1 to both NO_PROXY and no_proxy at startup (or set both to * if either contains *), and the service applies the same merge to NO_PROXY/no_proxy saved in its environment. This is backwards compatible: only loopback traffic stops going through env proxies.

Concerns:

  • The description doesn't match the diff. It describes AllocConsoleWithOptions and hidden-console.* changes for Windows console flashing, and none of those files are in this PR. Its Fixes #52266 / Closes #59598 would auto-close two Windows console-window issues that this change doesn't touch. Please replace the description with one for the proxy change and drop those two references. The issue this actually fixes is #52054.
  • Core's type check breaks (RequestInfo in loopback.ts, see inline).
  • The NO_PROXY merge logic is duplicated between cli and desktop (see inline).

Fixes #52054
Related: #49796, #47041, #54523

Comment thread packages/client/src/loopback.ts Outdated
// Bun routes loopback fetch() calls through HTTP(S)_PROXY/ALL_PROXY unless proxy: false is set on the request;
// browsers and Node's fetch ignore the extra field.
export const loopbackAwareFetch = Object.assign(
(input: RequestInfo | URL, init?: RequestInit) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestInfo is a DOM global that core's tsconfig (types: ["bun"], no DOM lib) doesn't have. Core type-checks this file through its references, so bun run typecheck:tsc in packages/core now fails with TS2552: Cannot find name 'RequestInfo' (it passes on base). Parameters<typeof globalThis.fetch>[0] (or string | URL | Request) avoids that.

@@ -0,0 +1,34 @@
// Bun and Node's EnvHttpProxyAgent read lowercase `no_proxy` alone whenever set, so both spellings
// receive one merged list. Preserves `*` intact because EnvHttpProxyAgent checks `noProxy === "*"`.
export function ensureLoopbackNoProxy() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same merge as Env.ensureLoopbackNoProxy in packages/cli/src/env.ts. Both packages already depend on @opencode/client, which now has the matching loopback.ts. Keeping one copy there (taking an env record, like the CLI version) would stop the two lists drifting apart, for example when the next wildcard or IPv6 edge case gets fixed in only one of them.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The earlier type-check issue is fixed: core's typecheck:tsc and the client's tsc are clean again, and the loopback-proxy tests still pass. The updated description now correctly targets #52054. The duplicated NO_PROXY merge between cli and desktop is still open in its thread. Nothing new to raise.

Public API is unchanged from my last review: @opencode/client OpenCode.make still bypasses env proxies for loopback URLs when no custom fetch is passed, and the CLI and Desktop still merge loopback hosts into NO_PROXY/no_proxy. This is backwards compatible.

This branch has not been deployed

No deployments
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.

client: event stream loops forever and TUI shows "Restarting service…" when HTTP proxy is set without no_proxy

2 participants