Skip to content

fix: port remaining applicable upstream fixes (timer guards, dead pool path) - #68

Merged
ronag merged 4 commits into
masterfrom
fix/upstream-sync-timer-guards
Aug 8, 2026
Merged

ronag merged 4 commits into
masterfrom
fix/upstream-sync-timer-guards

Conversation

@ronag

@ronag ronag commented Aug 8, 2026 •

Copy link
Copy Markdown
Member

Audit of nodejs/undici since 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 without refresh() / unref()

refreshTimeout() assumed every handle from the ambient setTimeout is a native Node Timeout. A shimmed setTimeout that returns a bare id makes both paths throw:

  • creation → fastNowTimeout.unref is not a function
  • reschedule → fastNowTimeout.refresh is not a function

Either one kills the FastTimer tick 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.js branches on JEST_WORKER_ID), so it is reachable. Ports nodejs#4213, plus a clearTimeout of the old handle before replacing it so a shim without refresh() can't leave a duplicate tick running.

Regression test added to test/timer-handles.js; it fails on master with TypeError: fastNowTimeout.unref is not a function.

fix(pool) — remove the unreachable kRemoveClient

kRemoveClient has had no callers since kDetachClient took over stale-client removal. It still carried two upstream bugs fixed after the fork point:

  • pre-nodejs/undici#5151 needDrain inversion — it set needDrain when a client was available
  • pre-nodejs/undici#5145 deferred splice (removal only landing in the close() 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

Upstream Why not
nodejs#5620 setEncoding() body truncation Already fixed here by da650fc4 via kPreservedBuffer + the push() override. Upstream reworked it to re-encode state.buffer instead; behaviourally equivalent, and swapping it out is a hot-path refactor with no behaviour gain.
nodejs#5606 / nodejs#5499 / nodejs#5397 idle socket validation Deliberate divergence — this fork keeps a referenced setImmediate (f4724ec9), which is our still-open nodejs#5609. Upstream reverted to an unref'd setTimeout(0).
nodejs#5577 DNS origin hostname on sockets Needs the DNS interceptor, which this fork doesn't ship. The client[kServerName] half is already covered by the https: servername-change branch in _resume.
nodejs#5547 / nodejs#4831 IP prioritization hints, nodejs#4175 + nodejs#4807 clientTtl, nodejs#4365 maxOrigins, nodejs#4296 pingInterval Features this fork doesn't carry.
nodejs#5231 writeBlob assert, nodejs#5120 Content-Range, nodejs#4311 maxRedirections, nodejs#4289 body diagnostics channels, all h2/websocket/proxy/cache/fetch work Code paths removed from this fork.
nodejs#5273, nodejs#5356, nodejs#5375, nodejs#5389, nodejs#5060, nodejs#5062, nodejs#5007, nodejs#4923, nodejs#4937, nodejs#4775, nodejs#4758, nodejs#4931 CRLF/dup-header validation, 89323ff pipelined response errors, nodejs#5151 in dispatch(), nodejs#5125 parser WeakRef, nodejs#5459 QUERY Verified already present.

Verification

  • borp --expose-gc -p "test/*.js" — 570 pass, 0 fail, 6 skip
  • borp -p "test/node-test/**/*.js" — 151 pass, 0 fail
  • tsc -p test/types/tsconfig.json — clean
  • eslint --no-cache . — clean

All validation ran locally on Node.js 26.5.0.

🤖 Generated with Claude Code

ronag and others added 2 commits August 8, 2026 16:41
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>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@ronag
ronag marked this pull request as draft August 8, 2026 13:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 without refresh() / 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 kRemoveClient symbol/method/export from PoolBase.

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.

Comment thread lib/util/timers.js Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@ronag
ronag marked this pull request as ready for review August 8, 2026 14:02
@ronag
ronag merged commit a2db18c into master Aug 8, 2026
1 check 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