Skip to content

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

Description

@reno-orange

Description

When http_proxy/https_proxy are exported and no_proxy is empty, the TUI routes its
traffic to its own background service on 127.0.0.1 through the proxy. The proxy does
not deliver text/event-stream responses, so the /api/event stream is never received:
the client stalls, reconnects in a loop, and the TUI shows
"Restarting service… / Your session will resume automatically." indefinitely.
Plain JSON calls still work, which makes the failure look like a random "server restarting" bug.

Plugins

none

OpenCode version

2.0.19

Steps to reproduce

  1. Export a forward proxy and leave no_proxy unset:
    export http_proxy=http://localhost:3128; export https_proxy=http://localhost:3128
  2. Start the TUI: opencode (it spawns opencode serve --service on http://127.0.0.1:<port>).
  3. Check where the TUI is actually connected:
    ss -tnp | grep <tui-pid> → all sockets go to 127.0.0.1:3128, none to 127.0.0.1:<port>.
  4. Stay idle for 60–105 s, then watch ~/.local/share/opencode/log/opencode.log.
  5. The overlay "Restarting service…" appears and never clears.

Screenshot and/or share link

No response

Operating System

Ubuntu 24.04

Terminal

No response

Activity

  1. github-actions commented on Sep 29, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. r266-tech commented on Sep 29, 2026

    @r266-tech
    Contributor

    I reproduced the proxy routing with the real Promise client at v2.0.19 (1fd016ef3), Bun 1.4.2 on macOS. Two temporary loopback servers recorded requests; no model calls or managed-service restart was involved:

    Environment /api/info /api/event
    HTTP(S)_PROXY set, both NO_PROXY variants empty proxy proxy
    Same proxy, both NO_PROXY variants set to 127.0.0.1 direct direct

    Both endpoints use the generated client's same fetch path. This confirms the routing part of the report; it does not reproduce the proxy dropping SSE or the later restart loop. I also tried injecting fetch(url, { ...init, proxy: "" }): on this Bun version that still used the environment proxy, so an empty proxy string is not a verified fix.

    #49805 only rewrites wildcard addresses; this fixture already used 127.0.0.1. @kommander, is a managed-service-specific bypass already in progress? I can contribute this regression fixture without overlapping that work. Explicit remote-server and provider proxy settings should remain separate.

    AI-assisted investigation; all requests used synthetic local fixtures.

  3. Epslion404 commented on Oct 1, 2026

    @Epslion404

    Windows CLI v2.0.21 — same root cause, plus the resulting managed-service churn

    Environment:

    • Windows 24H2, build 26100.7462
    • @opencode/[email protected] (npm global), channel latest
    • HTTP_PROXY / HTTPS_PROXY set to a local proxy (http://127.0.0.1:7897), NO_PROXY unset
    • WinINET system proxy is disabled (ProxyEnable=0), so this is purely the env-var path in Bun's fetch

    What I observed on the CLI (not Desktop):

    • opencode service status / opencode api get /api/info never reached the registered service at http://127.0.0.1:123, while a direct request to the same URL returned 200 in ~70 ms.
    • A synthetic proxy that logs requests received GET http://127.0.0.1:123/api/info (absolute-form, proxy style); a synthetic service listening on 127.0.0.1:123 received nothing.
    • Because the probe never reaches the service, service status spends requestTimeout: 2000 and reports stopped; ensure() accumulates 3 timeouts and logs Background service is unresponsive; recovery cannot preserve persistent terminals, then SIGTERMs the healthy service and spawns replacements.

    Resulting failure chain (overlaps #51216 / #37795):

    • Replacements are spawned detached: true; if the parent dies, the service survives, but with a stale/absent registration the next client cannot adopt it.
    • New contenders then fail with Managed service port 123 on 127.0.0.1 is already in use by another process ...; the TUI ends with Timed out waiting for the background service to start and never opens.
    • Recovery required manually killing the orphaned serve --service and removing the stale state/opencode/service.json.

    Verified workarounds at this version/build:

    • NO_PROXY=127.0.0.1,localhost,::1 (user environment): discovery goes direct again (~0.25 s), warm connect works. On this Windows build the v2.0.21 binary did honor NO_PROXY (verified with a fake proxy and a fake service; also verified from a clean native scheduled-task environment).
    • opencode service set disabled true: plain opencode then runs a private server per launch (serve --stdio --port 0), which sidesteps the shared-service churn; validated end-to-end from a native environment.

    Suggested fixes:

    • Bypass the proxy for loopback in the service client, or document the NO_PROXY requirement.
    • Distinguish proxy transport errors from "service unresponsive" before terminating a registered service.
    • Self-evict orphaned services (pid/port ownership) instead of leaving a dead registration and a port conflict behind.

    Related issues: #51216, #37795, #49095, #41696.

  4. xolotlatoani commented on Oct 20, 2026

    @xolotlatoani

    i had NO_PROXY set with localhost and 0.0.0.0 and that didnt work. It only worked when I added 127.0.0.1

  5. ddgmma commented on Oct 22, 2026

    @ddgmma

    Following up from #57017 (closed as a duplicate of this one). I re-tested on v2.1.2 in WSL2 and the loopback request is still routed through the proxy when NO_PROXY uses the Windows wildcard form.

    Environment:

    • WSL2 Ubuntu 24.04, networkingMode=mirrored, Windows 11, Clash Verge on 127.0.0.1:7890
    • WSL autoProxy imports the Windows proxy settings, so the inherited value looks like:
    NO_PROXY=172.31.*,127.*,localhost
    no_proxy=172.31.*,127.*,localhost
    

    127.* does not match 127.0.0.1 here, so the probe is sent to the proxy. Only an explicit 127.0.0.1 entry works (localhost alone was not enough for me either, same as @xolotlatoani's report above).

    A/B test on v2.1.2, with a healthy managed service already listening on 127.0.0.1:49374:

    $ opencode --version
    v2.1.2
    
    $ ss -ltnp | grep 49374
    LISTEN 0 512 127.0.0.1:49374 0.0.0.0:*
    
    $ opencode service status          # NO_PROXY=127.0.0.1,localhost,::1
    http://127.0.0.1:49374
    
    $ env -u NO_PROXY -u no_proxy opencode service status
    stopped                            # returns in ~0.1s even though the service is up
    
    $ curl http://127.0.0.1:49374/                 # routed through the proxy
    502
    $ curl --noproxy '*' http://127.0.0.1:49374/
    200
    

    The failure chain is the same as in the original report: probe fails -> service is considered stopped -> a second serve --service is spawned -> EADDRINUSE -> after ~2 minutes the client reports Managed service port 49374 on 127.0.0.1 is already in use by another process.

    So this is still reproducible on 2.1.2. Adding 127.0.0.1 (and ::1) to NO_PROXY/no_proxy before startup, as done in #57030 / #57744, would cover the Windows-wildcard case too. Until that lands, the workaround is:

    export NO_PROXY=127.0.0.1,localhost,::1
    export no_proxy="$NO_PROXY"
  6. iceteaSA commented on Oct 23, 2026

    @iceteaSA

    One more effect of this bug: the requests that go through the proxy contain the service password.

    On 2.1.3 (Linux), I set HTTP_PROXY to a recording proxy and did not set NO_PROXY. Then opencode session list and opencode plugin list each sent approximately 1,250 requests to the proxy and then stopped with an error. Each request was GET http://127.0.0.1:<port>/api/info with the same Authorization: Basic ... header. That header contains the service password, so the proxy gets the password.

    A proxy that logs headers keeps the password. The service listens only on 127.0.0.1, so the risk is low. But the password must not go off the machine.

    With NO_PROXY=127.0.0.1,localhost,::1, the two commands worked and the proxy got 0 requests. With only HTTPS_PROXY set, the proxy also got 0 requests.

    On dev at 09e6ddf, the probe is a plain fetch that does not go around the proxy for loopback: packages/client/src/service-probe.ts:55. The header comes from headers() at lines 92-97, and packages/client/src/promise/service.ts:18-24 calls the probe.

    The client must send its loopback service requests directly, not through the proxy. That change stops the loop in this issue. It also keeps the password on the machine.

  7. r266-tech commented on Oct 24, 2026

    @r266-tech
    Contributor

    I checked the new credential case against #57030 with Bun 1.4.4 on macOS. The regression now runs both session list and plugin list against an isolated registered service with a synthetic password. With the startup bypass, both complete and the proxy sees zero requests; removing it sends the fixture's Basic header to the proxy in both cases.

    Pushed the expanded regression in 3920b7e. CLI suite: 340 passed, 9 skipped; the six-test file also passed 10 repetitions. This verifies routing/authentication in local fixtures, not the Linux 2.1.3 request count or the restart loop. Standalone SDK consumers that bypass the CLI entrypoint are outside this fix.

    AI-generated investigation; no real credentials, model calls or live service were used.

  8. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I can reproduce this on 2.0.19, and it still happens on the latest dev (86ce19d).

    Steps:

    • Run a forward proxy on localhost:3128 that buffers responses, so it never passes a text/event-stream through.
    • Export http_proxy/https_proxy pointing at it, with no no_proxy.
    • Start the TUI.

    Every TUI request to its own local server (/api/info, /api/plugin, /api/fs/list, and /api/event again and again) goes through the proxy instead of straight to 127.0.0.1. The event stream never arrives, and the reconnect overlay appears within seconds and never clears. In my setup the TUI wasn't running a managed background service, so the overlay read "Connection lost…" instead of "Restarting service…"; it's the same component with the same cause. Without the proxy variables, nothing goes through the proxy and the overlay doesn't appear.

    The cause seems to be that the runtime's fetch (Bun) uses HTTP_PROXY even for loopback addresses, and OpenCode doesn't bypass the proxy for its own local server.

    Until this is fixed, setting no_proxy=127.0.0.1,localhost (and NO_PROXY) should avoid it.

    The recording, a screenshot and the drive script are attached below.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Run with a buffering forward proxy on 127.0.0.1:3128 (proxy.py) and:
    //   http_proxy=http://localhost:3128 https_proxy=http://localhost:3128 \
    //   OPENCODE_DRIVE_MEDIA_DIR=/workspace/media opencode-drive run drive.ts
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
      },
      ({ ui }) =>
        Effect.gen(function* () {
          yield* ui.screenshot("start")
          for (let i = 1; i <= 14; i++) {
            yield* Effect.sleep("10 seconds")
            const frame = yield* ui.capture()
            const text = JSON.stringify(frame)
            const hit = /Restarting service|Connection lost/.test(text)
            yield* Effect.log(`t=${i * 10}s overlay=${hit}`)
            if (hit) {
              yield* ui.screenshot("overlay")
              yield* Effect.sleep("5 seconds")
              return
            }
          }
          yield* ui.screenshot("end")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  9. iceteaSA commented on Oct 28, 2026

    @iceteaSA

    Open PR #57030 targets this cause. It starts the loopback proxy bypass when the CLI starts (packages/cli/src/index.ts), and the TUI runs in that process. The PR moves the bypass from Desktop into a shared module (packages/util/src/proxy-env.ts).

    Its regression test sends api get /api/info to a loopback server through a recording proxy, and the proxy must get no requests. The reproduction above also includes the TUI event stream (/api/event), which that test does not include. A second test for that stream can include the condition that the bot reproduced.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions