Repository navigation
fix(runtime): bound proxied dispatcher teardown in the remaining call sites - #6003
Conversation
… sites Two call sites build their own proxy dispatcher and dispose it themselves, so the abort-before-close teardown pattern that the shared transport fix reordered (apache#5900) survives in them: - testProxyConnection's finally aborts the controller and then awaits the dispatcher's graceful close. Behind a real proxy that close never settles, so the call stays pending long past its 8s probe timeout and wedges the serialized settings lane it occupies on the Desktop. Start the dispatcher teardown before the abort and race it against a 1s grace, mirroring the transport pattern. - proxiedFetch's error path awaits the same graceful close after aborting, so a stalled close delays the rejection the caller is owed. Apply the same ordering and bound there. The success path keeps its fire-and-forget close, which owns the streaming body lifecycle and is intentionally not bounded. Regression tests stall ProxyAgent.prototype.close (the same Undici/Node-version-independent technique the transport tests use) and assert both calls still settle; both tests fail on the previous ordering. Refs apache#5978 Generated-by: GLM-5.3-Flash (ZCode)
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Full review of c4730b24. The reordering is correct: close()/destroy() starts synchronously before abort(). teardown never rejects, so the bounded race can't replace the probe result, and the grace timer is cleared on both outcomes. Both regression tests stub ProxyAgent.prototype.close and restore it via t.mock. There's no overlap with #5900 (it touches only scoped-fetch-transport.ts).
One P3 (inline): after the grace expires, the dispatcher is never escalated to destroy().
CI test passes, and the PR is mergeable. I did not run the tests or a real proxy.
…he grace expires Root cause: the bounded dispatcher teardown in testProxyConnection and the proxiedFetch error path raced the graceful close against a 1s grace, but when the grace won, the dispatcher was simply abandoned with its close() still pending, leaking the agent (flagged as a P3 inline on apache#6003). Fix: track who wins the grace race and, when it expires, fire-and-forget destroy() with a distinguishing reason. The destroy error is swallowed so teardown can never replace the result or rejection the caller is already owed. The grace duration and its external semantics are unchanged; the success path of proxiedFetch keeps its unbounded close, which owns the streaming body lifecycle. Verified: new regressions in proxy-test.test.ts and proxied-fetch.test.ts stub ProxyAgent#close to never settle and assert destroy is called; both failed before the fix (callCount 0) and pass after. runtime network+bots suites 167/167, biome check clean, check:asf-headers clean. Generated-by: GLM-5.3-Flash (ZCode)
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Incremental review: c4730b2 to 83a5cf8. There was no rebase; the merge-base is still f7fa724. The delta is a single new commit, 83a5cf8, which touches the same 4 files (+106/-8) and nothing from main.
Prior findings
- P3 (after the grace expires, the dispatcher is left in a pending
close()and never destroyed): fixed. In bothproxy-test.ts(finally) and theproxied-fetch.tserror path, the race now reports whether the grace won. If it did,destroy()is called fire-and-forget with its rejection swallowed. This brings both sites in line with #5900's transport. - Nit (the grace race is duplicated in both files): still open, and the duplicated block is now about 20 lines. A shared helper would be worth considering, but this is not blocking.
Verified on the delta
teardowncannot reject becausedisposeDispatcherswallows errors from close/destroy. Soteardown.then(() => false)cannot reject either, and the race cannot throw out offinallyor replace the rejection the caller is owed. The grace timer is still cleared on both outcomes.- In undici 8.x (
^8.7.0; checked against 8.11.2),DispatcherBase.destroy(err)called without a callback runs inside anew Promiseexecutor. A synchronous failure therefore becomes a rejection, which.catch(() => {})swallows. Nothing can escape synchronously from thefinallyblock.destroy()after a pendingclose()is a legal transition and settles the pending close callbacks. - In
proxiedFetch,dispatchercan be undefined when the URL is bypassed. In that casedisposeDispatcherreturns immediately, so teardown always wins the race and the escalation branch cannot run. The cast is harmless. - The success path keeps its unbounded graceful close, which owns the streaming body. That is correct and unchanged.
- New tests: each stubs
ProxyAgent#closeto never settle anddestroyas a no-op, then assertsdestroywas called within a 5 swithTimeout. In proxy-test, port 1 is refused, so it takes the close branch rather than the timeout branch. In proxied-fetch,timeoutMs: 0with a proxy that resets the connection takes the close branch. Both exercise the new code, and the mocks restore throught.mock.
Findings
None at P0–P3 in this delta.
Status
CI test passes on 83a5cf8. The PR is MERGEABLE and BLOCKED only on review. No protocol files are touched, so there is no epoch impact. Not run locally.
Astro-Han
left a comment
There was a problem hiding this comment.
Approved at @Astro-Han's explicit request: the automated review of this exact head found no blocking (P0–P2) issues, and CI is green.
Summary
Refs #5978. Two call sites build their own proxy dispatcher and dispose it themselves, so the abort-before-close teardown that the shared transport fix (#5900) reordered survives in them:
testProxyConnection(packages/runtime/src/network/proxy-test.ts) aborted its controller and then awaited the dispatcher's gracefulclose(). Behind a real proxy that close never settles, so the call stays pending long past its 8s probe timeout — the Desktop "Test current configuration" button spins forever and the serialized settings lane it occupies wedges. The fix starts dispatcher teardown before the abort and races it against a 1s grace, mirroring the transport pattern.proxiedFetcherror path (packages/runtime/src/bots/proxied-fetch.ts) awaited the same graceful close after aborting, so a stalled close delays the rejection the caller is owed. Same ordering and bound applied.The success path of
proxiedFetchkeeps its fire-and-forget close: it owns the streaming body lifecycle (awaiting it was the previous streaming bug), and bounding or re-chaining it is the separate semantic decision the issue itself flags for maintainers. That is why this PR usesRefsinstead ofFixes— the dispatcher-lifecycle question on the success path stays open on the issue.Both regression tests stall
ProxyAgent.prototype.close— the same Undici/Node-version-independent technique the transport tests in #5900 use — and assert the calls still settle. Both fail on the previous ordering.Verification
node --test dist/network/__tests__/proxy-test.test.js(before fix, with new tests)settles after a successful probe even when the dispatcher close stallshung to its 5s guard; 3 existing tests passednode --test dist/bots/__tests__/proxied-fetch.test.js(before fix, with new tests)rejects promptly on request failure even when the dispatcher close stallshung to its 5s guard; 5 existing tests passednode --test dist/network/__tests__/proxy-test.test.js(after fix)node --test dist/bots/__tests__/proxied-fetch.test.js(after fix)node --test dist/network/__tests__/*.test.js dist/bots/__tests__/*.test.js(adjacent suites)npx biome checkon the 4 changed filesnpm run check:asf-headersformat:check, full runtime/runtime-host suitesAI use
Tool(s) and scope: GLM-5.3-Flash (ZCode) — issue triage, root-cause confirmation against #5900's pattern, TDD implementation (tests written first and confirmed failing), and PR drafting; all checks executed locally on macOS arm64 (Node 24).
Checklist
Does this PR entail a change in behavior?