Skip to content

fix(daemon): honour HTTP(S)_PROXY when dialing the wakeup WebSocket - #6279

Merged
multica-eve merged 1 commit into
multica-ai:mainfrom
seacen:fix/daemon-wakeup-proxy
Aug 3, 2026
Merged

fix(daemon): honour HTTP(S)_PROXY when dialing the wakeup WebSocket#6279
multica-eve merged 1 commit into
multica-ai:mainfrom
seacen:fix/daemon-wakeup-proxy

Conversation

@seacen

@seacen seacen commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

On a machine whose only route to the internet is a corporate egress proxy, the daemon's task-wakeup WebSocket can never connect. runTaskWakeupConnection builds its dialer by hand, and a zero-value websocket.Dialer has Proxy == nil, which gorilla reads as "dial direct" — unlike websocket.DefaultDialer, it never consults the environment. The failure is silent, too: the loop logs "task wakeup websocket unavailable; polling fallback remains active" at debug level and nothing ever mentions a proxy, so the daemon just runs permanently degraded — task pickup waits on the 30s HTTP poll instead of a server push, and the WS-first claim path (MUL-4257) falls back to HTTP on every task.

Every net/http call in the process honours HTTPS_PROXY through DefaultTransport, so this one connection behaving differently is genuinely hard to diagnose from the outside.

The fix is one line — set Proxy: http.ProxyFromEnvironment — plus a comment. gorilla rewrites wss:// to https:// before consulting Proxy, so HTTPS_PROXY applies to this dial. With no proxy variables set, ProxyFromEnvironment returns nil and the dial is byte-for-byte what it was before; NO_PROXY is honoured the same way it is everywhere else in the process.

A note on the regression test, because its shape is unusual: it drives the real runTaskWakeupConnection dial through a fake CONNECT proxy, but it has to re-exec itself into a child process. net/http caches the proxy environment once per process (envProxyOnce), and other daemon tests have already burned that cache by the time this test runs — a plain t.Setenv version passes in isolation and fails with the package. The child gets a scrubbed environment with only the fake proxy set, so the cache semantics are correct regardless of test order or -shuffle. The target host is deliberately non-loopback, since Go's proxy rules bypass proxies for localhost. The test fails on main (the proxy never sees a CONNECT) and passes with the fix; it is also clean under -race.

go build ./..., go vet, and the wakeup/WS/heartbeat tests are green. One pre-existing failure in the package (TestProbeAgentCLIs_QoderResolvesViaLoginShell) fails identically on a clean main checkout on this machine — it picks up a locally installed qoder CLI — and is unrelated.

This is the last hand-built dialer in the tree; the lark connector already falls back to the environment at dial time (#4165). Follow-up to the note at the end of my review on #5833.

cc @Bohan-J — one line of production code, hopefully quick.

runTaskWakeupConnection built its dialer by hand:

    dialer := websocket.Dialer{HandshakeTimeout: 10 * time.Second}

A zero-value Proxy field means "dial direct and ignore the
environment". websocket.DefaultDialer sets Proxy to
http.ProxyFromEnvironment; a dialer built this way gets nothing, and
gorilla skips the CONNECT wrapper entirely.

This is the daemon's control connection to the Multica server, so in
the SaaS deployment it dials out to the public internet. On a machine
whose only egress is a corporate proxy the handshake can never succeed.
Nothing points at the cause either: the loop logs "task wakeup
websocket unavailable; polling fallback remains active" at debug level
and never mentions a proxy. The daemon then runs permanently degraded —
task pickup waits for the HTTP poll (PollInterval, 30s by default)
instead of a server push, heartbeats stay on HTTP, and the WS-first
batch claim path (MUL-4257) falls back to HTTP on every task.

Set Proxy: http.ProxyFromEnvironment. gorilla rewrites wss:// to
https:// on the parsed URL before it calls Proxy, so HTTPS_PROXY — and
NO_PROXY — apply to this dial the same way they apply to every other
HTTPS client in the process. The lark connector fixed the same defect
the same way in multica-ai#4165; this was the last bare dialer left in non-test
code.

No regression where no proxy is configured: ProxyFromEnvironment
returns a nil URL, gorilla leaves netDial untouched, and the dial is
byte-for-byte the direct dial it was before. Everything downstream of
the handshake — headers, heartbeat writer, RPC attach, teardown — is
unchanged.

The regression test drives the dial from a child process. net/http
resolves the proxy environment once per process and caches the result
(envProxyOnce), so by the time a test in this package runs, an earlier
test has already primed that cache with "no proxy" and t.Setenv can no
longer reach it. The child starts with a clean environment pointing at
a stub CONNECT proxy, and the parent asserts the CONNECT for the wss
target arrived. Without the fix the stub proxy sees nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

@seacen is attempting to deploy a commit to the IndexLabs Team on Vercel.

A member of the Team first needs to authorize it.

@multica-eve
multica-eve merged commit 829a4e5 into multica-ai:main Aug 3, 2026
9 checks passed
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.

2 participants