Repository navigation
fix(runtime): createProxiedFetchTransport().close() never settles when a proxy is configured, hanging connection verify #5897
Description
Activity
Correction: two inaccuracies in the original report
Reviewing this after posting, I re-verified every falsifiable claim against the
published packages. Two statements in the original report are wrong. Neither
changes the diagnosis; one removes evidence I do not actually have, and the other
misstates a cause.1. The TS source is not byte-identical to the shipped dist
The original report said the
packages/runtime/src/network/scoped-fetch-transport.ts
source and the shippeddist/network/scoped-fetch-transport.js"are identical in
0.2.0-dev.64.20260929". That is not true. The shipped dist is the compiled form:
types are stripped andimport typelines are elided.What I should have claimed — and have now verified — is that the
close()
implementation is the same in both:const close = (): Promise<void> => { if (closePromise) return closePromise; closed = true; connections.abort(new Error('Connection effect fetch transport closed')); closePromise = Promise.all([ directDispatcher.destroy(new Error('Connection effect fetch transport closed')).catch(() => {}), proxyDispatcher?.destroy(new Error('Connection effect fetch transport closed')).catch(() => {}), ]).then(() => undefined); return closePromise; };
The
close()body I quoted in the original report is accurate; only the
"identical" framing was wrong.2. "never returns; client gives up at 25s" — the 25s was my own harness
The original report's end-to-end table said the verify RPC "never returns; client
gives up at 25s". That 25-second figure came from my test script's own race
timeout, not from any timeout in Maka. I checked the shipped CLI to be sure:dist/runtime-host-onboarding.jscalls the RPC with no timeout argument:const result = await connection.request('connection.onboarding.verify', { target: input.target, apiKey: trimmedOrNull(input.apiKey), baseUrl: trimmedOrNull(input.baseUrl), });
(Contrast the OAuth paths in the same file, which do compute a
remainingTimeout()
and pass one toconnection.request(...).) So the accurate statement is: the
request has no client-side deadline, the RPC never returns, and the wizard's
"Verifying…" state persists indefinitely. My harness supplied the 25s.The underlying point stands and is if anything stronger — the absence of any
timeout on this call path is part of why the failure is a silent hang rather than
a visible error.What I have now verified independently
Re-checked against the published packages rather than my earlier notes:
#5043attribution holds. I fetched0.2.0-dev.51.20260925(pre-#5043)
and ran the same reproduction against it:close()returns in 1ms. Against
pristine0.2.0-dev.64.20260929(confirmed to contain no local modification)
the identical script leavesclose()still pending after 15s. dev.51 has
neitherconnections.abort()inclose()noronRequestUpgradein
proxy-dispatcher.js; dev.64 has both. So#5043did introduce this.- Proxy-gated behaviour holds. Same pristine dev.64 code: with
createProxiedFetchTransport(null)→close()settles in 1ms; with an HTTP
proxy → never settles. The no-proxy path is genuinely unaffected. - The bisect row holds. Against pristine dev.64:
factoryalone →
destroy()settles in 1ms;factory+ theclientFactoryoverriding
onRequestUpgrade(whatbuildProxyDispatcherbuilds) → still pending after
10s.
The minimal reproduction in the original report does need a reachable HTTP proxy,
since the failing path is a proxied CONNECT tunnel. That was stated.I have not built the repository or run its test suite, as noted in Provenance.
Local mitigation available, if useful for validating a fix
To be explicit up front: how upstream fixes this is entirely the maintainers'
decision. This comment only reports what we have actually measured on our own
machine, in case it is useful as a validation target or a starting point. We have
not built this repository or run its test suite, so treat the numbers below as
observations about the published package, not as a reviewed patch.What we applied locally
Two edits against the installed
maka-agent@0.2.0-dev.64.20260929, applied to
the shippeddist(not topackages/runtime/src). Both are recorded here in full
so they can be reproduced or dismissed.1. Bound dispatcher teardown —
@maka/runtime/dist/network/scoped-fetch-transport.js:+const TRANSPORT_CLOSE_GRACE_MS = 1_000; ... const close = () => { if (closePromise) return closePromise; closed = true; connections.abort(new Error('Connection effect fetch transport closed')); closePromise = Promise.all([...]).then(() => undefined); - return closePromise; + return Promise.race([ + closePromise, + new Promise((resolve) => setTimeout(resolve, TRANSPORT_CLOSE_GRACE_MS, undefined)), + ]); };
2. Give the verify/save RPCs a deadline —
dist/runtime-host-onboarding.js:+const ONBOARDING_VERIFY_TIMEOUT_MS = 30_000; +const ONBOARDING_SAVE_TIMEOUT_MS = 30_000; ... - const result = await connection.request('connection.onboarding.verify', { … }); + const result = await connection.request('connection.onboarding.verify', { … }, ONBOARDING_VERIFY_TIMEOUT_MS);
Edit 2 exists only because edit 1 means
close()can no longer hang forever; it is
defence in depth, so that any future wedge on this path surfaces as the
wizard's existingunavailablefailure (which renders via
copy.onboardingRequestFailedindist/pi-tui-pickers.js) instead of an
indefinite "Verifying…" state.Measured effect
Same reproduction as above, against the published package, proxy reachable:
pristine dev.64 with edit 1 close()after one proxied fetchpending after 15s ~1.0s connection.onboarding.verifyover RPCno return ~1.5s, kind: 'verified', 39 modelsHost activeOperationsafter the call+1, never released flat across 5 consecutive calls The
activeOperationsrow is the one that matters most for us: with the pristine
build a single wedge wedges that provider's lane for the life of the Host process,
so every later attempt queues behind it. We also completed the real onboarding
wizard end-to-end twice against a real endpoint — the flow reaches the model
picker and persists the connection, where it previously stopped at the verify step.One thing we checked before recommending it, since a bounded
close()could plausibly leakBounding teardown means
close()can return while undici'sdestroy()is still
pending, so we measured whether that retains sockets. Three cycles of
create → one proxied fetch →close(), counting/proc/self/fd:- pristine dev.64: 23 fds before, 23 after, flat across all three cycles
- with edit 1: 23 fds before, 23 after, flat across all three cycles
- both processes exited on their own in both cases
So on this path we saw no descriptor retention, but that is a single observation
on one platform and one proxy type, not a proof. The SOCKS path builds its own
Agentand was never affected, so we did not test it under edit 1.Provenance
Same as the original report: this investigation and this comment were produced
with the assistance of an AI coding assistant, disclosed per
CONTRIBUTING.md. Every
number above was re-measured against the pristine published package, not against
our own patched working tree.
Summary
createProxiedFetchTransport().close()in@maka/runtimenever settles when a network proxy is configured. It aborts the sharedAbortControllerfirst, which destroys the proxied CONNECT tunnel socket out from under undici'sProxyAgent;ProxyAgent.destroy()then never resolves, so every caller that awaitsclose()in afinallyhangs forever.The most visible consequence is that adding a custom connection in the CLI hangs indefinitely at the verify step, with no error and no timeout —
connection.onboarding.verifynever returns, and the Host's per-provider lane stays wedged for the life of the process.This only reproduces with
policy.networkProxy.enabled === true. Without a proxy the direct dispatcher settles normally. Introduced by #5043.Affected code
packages/runtime/src/network/scoped-fetch-transport.ts(shipped asdist/network/scoped-fetch-transport.js; the two are identical in0.2.0-dev.64.20260929):(1) destroys the CONNECT tunnel socket, because #5043 registers that socket with
abortSocket(signal, socket)inbuildProxyDispatcher'sclientFactory:undici never observes that external
socket.destroy(), so theProxyAgentclient promise stays unsettled and (2) never settles, making (3) hang.Reproduction
Environment: maka-agent
0.2.0-dev.64.20260929, undici8.11.2, Node 26.7.0, HTTP proxy on127.0.0.1:10808,policy.networkProxy.enabled = true.Minimal, no proxy fixtures needed — a real proxy and a real endpoint:
Observed: fetch succeeds,
close()still pending after 20s. The same script withtype/proxy disabled, or against a barenew ProxyAgent(...)instead of this wrapper, closes in ~1ms.Evidence narrowing the cause
Bisecting the dispatcher construction, with
ac.abort()performed beforedestroy()in each case:destroy()ProxyAgent+ factory(cancellable connect)+ clientFactoryoverridingonRequestUpgrade— what Maka buildsProbes injected into a running Host isolate it to the exact await:
So the discovery work is fine; only teardown wedges.
buildSocks5Dispatcherbuilds its ownAgentand is not affected — this is specific to the HTTP proxyProxyAgentpath.End-to-end impact
Driving the real CLI onboarding flow (custom connection → base URL → API key), the verify step never returns:
connection.onboarding.verifyover RPCkind: 'verified', 39 modelsactiveOperationsThe second row matters beyond the single request: because the lane is never released, every later create for the same provider queues behind it, so one wedge poisons the rest of the process's lifetime.
Suggested fix
Teardown must not gate the caller. The sockets are already aborted by (1); awaiting undici's dispatcher teardown afterwards is best-effort and should be bounded, e.g. race the destroy against a short grace period:
Verified locally:
close()returns in ~1s, verify completes in ~1.5s with all 39 models, andactiveOperationsstays flat over repeated runs.Fixing it at the root instead — having
abortSocketnotify undici rather than unilaterally destroying the socket — would also be reasonable, and #5043's own follow-up note about releasing cancellation listeners suggests that area is still in flux.Notes
connection.onboarding.verifyitself; the RPC is fine, itsfinallyjust never completes.packages/runtime/src/network/__tests__/scoped-fetch-transport.test.tsthat configures a proxy, performs one fetch, and assertsclose()settles.Provenance
Generative tooling contributed substantively to this analysis. The investigation (source reading, instrumented probes, bisection, reproduction scripts) was performed with an AI coding assistant, and this report was drafted with its assistance. All version numbers, the
close()source, and the bisection results were verified directly against the repository and the publishedmaka-agent@0.2.0-dev.64.20260929package. The suggested patch is a mitigation derived from that analysis and is offered for discussion, not as a reviewed contribution — I have not built or run the repository's test suite, so please treat the diagnosis as the reportable part rather than a finished change.