You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
client: event stream loops forever and TUI shows "Restarting service…" when HTTP proxy is set without no_proxy #52054
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
Export a forward proxy and leave no_proxy unset: export http_proxy=http://localhost:3128; export https_proxy=http://localhost:3128
Start the TUI: opencode (it spawns opencode serve --service on http://127.0.0.1:<port>).
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>.
Stay idle for 60–105 s, then watch ~/.local/share/opencode/log/opencode.log.
The overlay "Restarting service…" appears and never clears.
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.
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.
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.
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:
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:
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.
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.
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
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.tsimport{Effect}from"effect"import{OpenCodeDriver}from"opencode-drive"exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true},},({ ui })=>Effect.gen(function*(){yield*ui.screenshot("start")for(leti=1;i<=14;i++){yield*Effect.sleep("10 seconds")constframe=yield*ui.capture()consttext=JSON.stringify(frame)consthit=/Restartingservice|Connectionlost/.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.
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.
Activity
github-actions commentedon Sep 29, 2026
This issue might be a duplicate of existing issues. Please check:
opencode attachsends loopback requests through HTTP(S)_PROXY even when NO_PROXY includes localhost; related local-service proxy bypass behavior.r266-tech commentedon Sep 29, 2026
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:/api/info/api/event127.0.0.1Both endpoints use the generated client's same
fetchpath. This confirms the routing part of the report; it does not reproduce the proxy dropping SSE or the later restart loop. I also tried injectingfetch(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.
Epslion404 commentedon Oct 1, 2026
Windows CLI v2.0.21 — same root cause, plus the resulting managed-service churn
Environment:
@opencode/[email protected](npm global), channellatestHTTP_PROXY/HTTPS_PROXYset to a local proxy (http://127.0.0.1:7897),NO_PROXYunsetProxyEnable=0), so this is purely the env-var path in Bun's fetchWhat I observed on the CLI (not Desktop):
opencode service status/opencode api get /api/infonever reached the registered service athttp://127.0.0.1:123, while a direct request to the same URL returned200in ~70 ms.GET http://127.0.0.1:123/api/info(absolute-form, proxy style); a synthetic service listening on127.0.0.1:123received nothing.service statusspendsrequestTimeout: 2000and reportsstopped;ensure()accumulates 3 timeouts and logsBackground service is unresponsive; recovery cannot preserve persistent terminals, then SIGTERMs the healthy service and spawns replacements.Resulting failure chain (overlaps #51216 / #37795):
detached: true; if the parent dies, the service survives, but with a stale/absent registration the next client cannot adopt it.Managed service port 123 on 127.0.0.1 is already in use by another process ...; the TUI ends withTimed out waiting for the background service to startand never opens.serve --serviceand removing the stalestate/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 honorNO_PROXY(verified with a fake proxy and a fake service; also verified from a clean native scheduled-task environment).opencode service set disabled true: plainopencodethen 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:
NO_PROXYrequirement.Related issues: #51216, #37795, #49095, #41696.
xolotlatoani commentedon Oct 20, 2026
i had NO_PROXY set with
localhostand0.0.0.0and that didnt work. It only worked when I added127.0.0.1ddgmma commentedon Oct 22, 2026
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_PROXYuses the Windows wildcard form.Environment:
networkingMode=mirrored, Windows 11, Clash Verge on127.0.0.1:7890127.*does not match127.0.0.1here, so the probe is sent to the proxy. Only an explicit127.0.0.1entry works (localhostalone 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:The failure chain is the same as in the original report: probe fails -> service is considered stopped -> a second
serve --serviceis spawned ->EADDRINUSE-> after ~2 minutes the client reportsManaged 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) toNO_PROXY/no_proxybefore startup, as done in #57030 / #57744, would cover the Windows-wildcard case too. Until that lands, the workaround is:iceteaSA commentedon Oct 23, 2026
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_PROXYto a recording proxy and did not setNO_PROXY. Thenopencode session listandopencode plugin listeach sent approximately 1,250 requests to the proxy and then stopped with an error. Each request wasGET http://127.0.0.1:<port>/api/infowith the sameAuthorization: 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 onlyHTTPS_PROXYset, the proxy also got 0 requests.On
devat 09e6ddf, the probe is a plainfetchthat does not go around the proxy for loopback:packages/client/src/service-probe.ts:55. The header comes fromheaders()at lines 92-97, andpackages/client/src/promise/service.ts:18-24calls 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.
r266-tech commentedon Oct 24, 2026
I checked the new credential case against #57030 with Bun 1.4.4 on macOS. The regression now runs both
session listandplugin listagainst 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.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I can reproduce this on 2.0.19, and it still happens on the latest
dev(86ce19d).Steps:
localhost:3128that buffers responses, so it never passes atext/event-streamthrough.http_proxy/https_proxypointing at it, with nono_proxy.Every TUI request to its own local server (
/api/info,/api/plugin,/api/fs/list, and/api/eventagain and again) goes through the proxy instead of straight to127.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) usesHTTP_PROXYeven 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(andNO_PROXY) should avoid it.The recording, a screenshot and the drive script are attached below.
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.iceteaSA commentedon Oct 28, 2026
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/infoto 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.