Repository navigation
fix: port remaining applicable upstream fixes (timer guards, dead pool path) - #68
Conversation
refreshTimeout() assumed every handle returned by the ambient setTimeout is a native Node Timeout. Fake-timer polyfills (and any environment that shims setTimeout) can hand back a bare id, so both the reschedule path (refresh()) and the creation path (unref()) threw a TypeError and killed the FastTimer tick loop for the rest of the process. Probe for both methods instead, and clear the previous handle before replacing it so a shim without refresh() cannot leave a duplicate tick running. Ports nodejs#4213. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing has called kRemoveClient since kDetachClient took over stale client removal; it is dead code carrying the pre-nodejs/undici#5151 needDrain inversion (it set needDrain when a client *was* available) and the pre-nodejs#5145 deferred splice. Rather than port fixes into a method with no callers, remove it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Claude Code Review
Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Pull request overview
Ports two remaining upstream fixes into this fork: (1) makes the FastTimer tick-loop resilient to environments where setTimeout returns non-Node timer handles, and (2) removes an unused/stale PoolBase client-removal path.
Changes:
- Harden
refreshTimeout()against timer handles withoutrefresh()/unref(), and clear the previous tick handle before replacing it. - Add a regression test covering “bare id” timer handles to ensure only one tick is scheduled.
- Remove the dead
kRemoveClientsymbol/method/export fromPoolBase.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| test/timer-handles.js | Adds coverage for mocked timer handles that don’t implement Node’s timer lifecycle methods. |
| lib/util/timers.js | Makes FastTimer’s backing tick scheduling tolerate missing refresh()/unref() and avoids duplicate ticks. |
| lib/dispatcher/pool-base.js | Drops the unused kRemoveClient path and its export to reduce dead/buggy surface. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Audit of
nodejs/undicisince the fork point (a180465f, v7.5.0 — 842 upstream commits, 125 touching files this fork still ships) for anything applicable but missing here. Nearly everything relevant is already in — often via this fork's own commits, sometimes ahead of upstream. Two real gaps came out of it.fix(timers)— handles withoutrefresh()/unref()refreshTimeout()assumed every handle from the ambientsetTimeoutis a native NodeTimeout. A shimmedsetTimeoutthat returns a bare id makes both paths throw:fastNowTimeout.unref is not a functionfastNowTimeout.refresh is not a functionEither one kills the
FastTimertick loop for the rest of the process, so every headers/body/keep-alive timeout silently stops firing. This fork explicitly supports mocked environments (client-h1.jsbranches onJEST_WORKER_ID), so it is reachable. Ports nodejs#4213, plus aclearTimeoutof the old handle before replacing it so a shim withoutrefresh()can't leave a duplicate tick running.Regression test added to
test/timer-handles.js; it fails onmasterwithTypeError: fastNowTimeout.unref is not a function.fix(pool)— remove the unreachablekRemoveClientkRemoveClienthas had no callers sincekDetachClienttook over stale-client removal. It still carried two upstream bugs fixed after the fork point:needDraininversion — it setneedDrainwhen a client was availableclose()callback)Rather than port fixes into a method nothing calls, removed it and its symbol/export. Alternative, if you'd rather keep the surface: negate the
some()and splice synchronously to match upstream.Checked and deliberately not ported
setEncoding()body truncationda650fc4viakPreservedBuffer+ thepush()override. Upstream reworked it to re-encodestate.bufferinstead; behaviourally equivalent, and swapping it out is a hot-path refactor with no behaviour gain.setImmediate(f4724ec9), which is our still-open nodejs#5609. Upstream reverted to an unref'dsetTimeout(0).client[kServerName]half is already covered by thehttps:servername-change branch in_resume.clientTtl, nodejs#4365maxOrigins, nodejs#4296pingIntervalwriteBlobassert, nodejs#5120Content-Range, nodejs#4311maxRedirections, nodejs#4289 body diagnostics channels, all h2/websocket/proxy/cache/fetch work89323ffpipelined response errors, nodejs#5151 indispatch(), nodejs#5125 parserWeakRef, nodejs#5459 QUERYVerification
borp --expose-gc -p "test/*.js"— 570 pass, 0 fail, 6 skipborp -p "test/node-test/**/*.js"— 151 pass, 0 failtsc -p test/types/tsconfig.json— cleaneslint --no-cache .— cleanAll validation ran locally on Node.js 26.5.0.
🤖 Generated with Claude Code