Skip to content

fix(node): bind Dispatcher methods so Agent survives undici.compose()#155

Merged
vadimpiven merged 2 commits into
mainfrom
fix/agent-compose-private-field
May 18, 2026
Merged

fix(node): bind Dispatcher methods so Agent survives undici.compose()#155
vadimpiven merged 2 commits into
mainfrom
fix/agent-compose-private-field

Conversation

@vadimpiven
Copy link
Copy Markdown
Owner

undici's Dispatcher.compose() returns a Proxy whose get trap forwards lookups to the wrapped dispatcher, so composed.destroy() ends up invoking Agent#destroy with this === proxy. The proxy doesn't carry the class's private-field brand, and the first this.#destroyed = true throws:

TypeError: Cannot write private member #a to an object whose class
did not declare it

Bind dispatch, close, and destroy in the constructor so they re-route to the real Agent regardless of how they're invoked.

Reported by a downstream integration (@milaboratories/pl-client) that calls agent.compose(interceptors.retry()).destroy() on a normal lifecycle path.

undici's `Dispatcher.compose()` returns a `Proxy` whose `get` trap
forwards lookups to the wrapped dispatcher, so `composed.destroy()`
ends up invoking `Agent#destroy` with `this === proxy`. The proxy
doesn't carry the class's private-field brand, and the first
`this.#destroyed = true` throws:

  TypeError: Cannot write private member #a to an object whose class
  did not declare it

Bind `dispatch`, `close`, and `destroy` in the constructor so they
re-route to the real Agent regardless of how they're invoked.

Reported by a downstream integration (@milaboratories/pl-client) that
calls `agent.compose(interceptors.retry()).destroy()` on a normal
lifecycle path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@semanticdiff-com
Copy link
Copy Markdown

semanticdiff-com Bot commented May 18, 2026

Review changes with  SemanticDiff

Changed Files
File Status
  mise.lock Unsupported file format
  packages/node/README.md Unsupported file format
  packages/node/export/agent.ts  0% smaller
  packages/node/tests/vitest/agent-edge-cases.test.ts  0% smaller

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the mise.lock file and modifies the Node.js package to support undici.compose() by binding lifecycle methods in the Agent constructor, supported by new edge-case tests. Project requirements are also updated, raising the minimum Node.js version to 22.12.0 and adding Undici ^8.0.0. Feedback suggests using Object.assign for method binding to avoid TypeScript errors and notes that dropping Node.js 20 support is a breaking change that should be clearly documented.

Comment thread packages/node/export/agent.ts
Comment thread packages/node/README.md Outdated
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves compatibility with undici.compose() by binding the dispatch, close, and destroy methods in the Agent constructor and adds corresponding lifecycle tests. It also updates the mise.lock file and modifies the project requirements in the README. Feedback was provided regarding the Node.js version update, which drops support for Node.js 20; this is a breaking change that may be unintentional and should be reviewed.

Comment thread packages/node/README.md Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 18, 2026

Greptile Summary

This PR fixes a TypeError thrown when lifecycle methods (close, destroy) are called on a dispatcher returned by undici.compose(), whose Proxy get trap invokes the method with this === proxy — causing private-field brand checks to fail. The fix binds dispatch, close, and destroy to the real Agent instance in the constructor.

  • packages/node/export/agent.ts: Three bind calls added at the top of the constructor; the approach is idiomatic and correct for this pattern.
  • packages/node/tests/vitest/agent-edge-cases.test.ts: Two regression tests cover the exact close/destroy-through-proxy scenario from the bug report.
  • packages/node/README.md: Drops Node.js 20 from the supported range and adds an explicit undici ^8.0.0 peer requirement; the Node 20 removal is a breaking change absent from the PR description.

Confidence Score: 4/5

The core fix is safe to merge; the only items worth a second look are the undocumented Node.js 20 support drop in the README and the absence of a proxy-routed dispatch test.

The agent.ts change is minimal and correctly addresses the private-field brand-check failure. The new tests cover the documented failure scenario. The README quietly removes Node.js 20 from the supported range, which is a breaking change for consumers on that LTS line and is not mentioned anywhere in the PR description or title.

packages/node/README.md — the Node.js 20 requirement drop should be confirmed as intentional and communicated to users.

Important Files Changed

Filename Overview
packages/node/export/agent.ts Binds dispatch, close, and destroy to this in the constructor; fix is minimal, correct, and targets the exact private-field brand-check failure caused by undici.compose() Proxy indirection.
packages/node/tests/vitest/agent-edge-cases.test.ts Adds two focused regression tests for the compose-proxy lifecycle; covers the exact failure scenario from the PR description.
packages/node/README.md Drops Node.js 20 (^20.19.0) from the supported range and adds an explicit undici ^8.0.0 peer requirement; the Node 20 drop is a breaking change not mentioned in the PR title or description.
mise.lock Removes github_attestations = "unavailable" entries for several tools; purely mechanical lockfile cleanup with no functional impact.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/node/README.md:153-154
**Undocumented breaking change: Node.js 20 support silently dropped**

The requirements block now lists only `>=22.12.0`, removing the previously supported `^20.19.0` range. This is a semver-breaking change for any consumer still on Node.js 20 LTS and is not mentioned in the PR title, description, or a changelog entry. If this is intentional (e.g. undici `^8.0.0` requires Node 22), it should be called out explicitly so downstream users know to expect the bump.

### Issue 2 of 2
packages/node/tests/vitest/agent-edge-cases.test.ts:250-263
**`dispatch` binding not exercised through the proxy**

The constructor now also binds `dispatch`, which accesses `this.#destroyed`, `this.#closed`, `this.#pending`, and `this.#maxBufferedRequestBodyBytes` — all private fields that would throw the same brand-check error if called with `this === proxy`. The two new tests only exercise `close` and `destroy` through the composed proxy; a test that sends a real request via `composed.dispatch(…)` (or the higher-level `composed.request(…)`) would give the same regression coverage for the `dispatch` binding.

Reviews (1): Last reviewed commit: "fix(node): bind Dispatcher methods so Ag..." | Re-trigger Greptile

Comment thread packages/node/README.md Outdated
Comment thread packages/node/tests/vitest/agent-edge-cases.test.ts
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 18, 2026

Merging this PR will not alter performance

✅ 15 untouched benchmarks


Comparing fix/agent-compose-private-field (35ed433) with main (026326c)

Open in CodSpeed

@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…ADME requirements

- Add regression test exercising a full request via `composed.dispatch()`
  through the undici compose Proxy (greptile P2). undici's `compose()`
  already short-circuits `dispatch` to the closure, so the brand check
  can't bite there today — but the constructor binds `dispatch` defensively
  and this test guards against future undici trap changes.

- Drop the README Requirements section. Source of truth is `package.json`
  (engines, peerDependencies). Keeps the README from drifting on every
  undici/Node bump and resolves the gemini/greptile "undocumented breaking
  change" feedback by removing the duplicated docs entirely.

The Gemini suggestion to use `Object.assign` to bind the methods is a
false positive — `tsc --noEmit` is clean with the direct assignments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vadimpiven vadimpiven merged commit 5377e0a into main May 18, 2026
15 of 18 checks passed
@vadimpiven vadimpiven deleted the fix/agent-compose-private-field branch May 18, 2026 07:42
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.

1 participant