Skip to content

feat: upgrade to undici@8#820

Merged
fengmk2 merged 16 commits into
masterfrom
fix/upgrade-undici-8
Jun 15, 2026
Merged

feat: upgrade to undici@8#820
fengmk2 merged 16 commits into
masterfrom
fix/upgrade-undici-8

Conversation

@fengmk2

@fengmk2 fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member

Upgrade undici from v7 to v8.

Breaking changes

  • Node.js >= 22.19.0 required (was >= 22.0.0).
  • HTTP/2 is negotiated by default. undici@8 flips allowH2 to default true, so requests to HTTP/2-capable TLS servers now use HTTP/2 instead of HTTP/1.1. Pass allowH2: false (per request or per client) to force HTTP/1.1.
  • allowH2: false is applied per request through the active dispatcher (default agent / ProxyAgent). It cannot downgrade a raw Pool/Client passed as dispatcher (construct those with allowH2: false), and is not applied under MockAgent.
  • HTTP/2 timeout error shape changed. An HTTP/2 headers timeout now surfaces as HeadersTimeoutError / UND_ERR_HEADERS_TIMEOUT (was InformationalError / UND_ERR_INFO); code matching on err.cause must update.
  • getDispatcherPoolStats() normalizes undici@8's ${origin}#http1-only pool keys back to the origin, so a single origin entry may merge HTTP/2 and HTTP/1.1 pool stats.

Other changes

  • CI matrix tests Node 22, 24, 26.
  • Request.ts: signal typed as globalThis.AbortSignal (v8 has a stricter signal type).

Closes #765

Summary by CodeRabbit

  • New Features
    • Added allowH2 option for per-request HTTP/2 protocol negotiation control (HTTP/2 is enabled by default).
    • Improved handling of connection pool statistics to correctly combine HTTP/2 and HTTP/1.1 data for the same origin.
  • Dependencies
    • Updated undici to ^8.4.1.
    • Raised Node.js minimum version to >= 22.19.0.
  • Tests / CI
    • Expanded protocol negotiation and pool/dispatcher tests; added Node.js 26 to the test matrix.
    • Added stricter Codecov patch coverage configuration.

undici@8 negotiates HTTP/2 by default (allowH2 now defaults to true), so
urllib uses HTTP/2 with capable servers unless a custom dispatcher opts out.
Raise the minimum Node.js version to 22.19.0 and test Node 22, 24, 26 in CI.

Test updates for undici@8 behavior changes:
- HTTP/2 headers timeout now surfaces as HeadersTimeoutError
- allowH2:true multiplexes requests over a single h2 connection
- close ProxyAgent instances in teardown to avoid a shutdown hang

Closes #765

BREAKING CHANGE: requires Node.js >= 22.19.0 and negotiates HTTP/2 by default
@fengmk2 fengmk2 self-assigned this Jun 14, 2026
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a067b9b6-eb32-4564-9582-21af8965a5e8

📥 Commits

Reviewing files that changed from the base of the PR and between d484db3 and a9a0b2b.

📒 Files selected for processing (4)
  • src/HttpClient.ts
  • src/index.ts
  • test/HttpClient.test.ts
  • test/options.timeout.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/options.timeout.test.ts
  • src/index.ts
  • src/HttpClient.ts
  • test/HttpClient.test.ts

📝 Walkthrough

Walkthrough

Upgrades undici from v7 to v8 and raises the Node.js minimum to 22.19.0. Adds allowH2?: boolean to RequestOptions for per-request HTTP/2 negotiation. Refactors HttpAgent to remove allowH2 from the connect object, adds normalizePoolStatsKey/mergePoolStat/buildPoolStats helpers to HttpClient, and refactors the default-client cache in index.ts to key on (rejectUnauthorized, allowH2). CI matrix is updated to Node.js 26 and a codecov.yml is added.

Changes

undici@8 Upgrade and allowH2 Negotiation

Layer / File(s) Summary
undici@8 and Node.js version bump
package.json
Bumps undici from ^7.24.0 to ^8.4.1 and raises engines.node to >= 22.19.0.
RequestOptions.allowH2 public contract
src/Request.ts
Removes local AbortSignal alias, adds allowH2?: boolean with JSDoc to RequestOptions, and updates signal type to globalThis.AbortSignal.
HttpAgent: remove allowH2 from connect options
src/HttpAgent.ts
Removes allowH2 from the connect object in HttpAgent's super call to prevent it from shadowing the top-level per-request setting.
Pool-stat helpers and HttpClient per-request allowH2
src/HttpClient.ts
Exports normalizePoolStatsKey, mergePoolStat, and buildPoolStats; stores client-level allowH2 in a private field; applies allowH2 per request (skipping MockAgent); delegates getDispatcherPoolStats() to buildPoolStats.
fetch.ts: wiring allowH2 and delegating pool stats
src/fetch.ts
Updates imports to include Dispatcher and buildPoolStats, tightens setClientOptions allowH2 condition to check !== undefined, delegates pool stats to buildPoolStats helper.
index.ts: refactor default-client caching by (rejectUnauthorized, allowH2)
src/index.ts
Rewrites getDefaultHttpClient to use a Map keyed by computed (rejectUnauthorized, allowH2) string, conditionally sets clientOptions.allowH2 and connect.rejectUnauthorized.
Test coverage for allowH2 negotiation, pool stats, and timeout
test/HttpClient.test.ts, test/fetch.test.ts, test/index.test.ts, test/options.dispatcher.test.ts, test/options.timeout.test.ts
Adds startH2EchoServer helper and full protocol negotiation suite with tests for H1/H2 selection, per-request allowH2: false forcing, dispatcher/caching semantics, pool-stat normalization/merging, and HttpAgent routing. Updates fetch/index/timeout tests with allowH2 assertions. Adds ProxyAgent teardown tracking.
CI and coverage config updates
.github/workflows/nodejs.yml, codecov.yml
Updates Node.js CI test matrix to 26, adds codecov.yml with strict patch coverage (0% threshold) and 1% project threshold.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant urllib_request as urllib.request()
  participant getDefaultHttpClient
  participant HttpClient
  participant undici_Dispatcher as undici Dispatcher

  Caller->>urllib_request: request(url, { allowH2: false })
  urllib_request->>getDefaultHttpClient: getDefaultHttpClient(rejectUnauthorized, allowH2)
  getDefaultHttpClient->>getDefaultHttpClient: lookup Map key "false:false"
  getDefaultHttpClient-->>urllib_request: cached or new HttpClient(allowH2: false)
  urllib_request->>HttpClient: request(url, { allowH2: false })
  HttpClient->>HttpClient: effectiveAllowH2 = args.allowH2 ?? `#allowH2`
  HttpClient->>undici_Dispatcher: dispatch({ allowH2: false }) [skip if MockAgent]
  undici_Dispatcher-->>HttpClient: HTTP/1.1 response
  HttpClient-->>Caller: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • node-modules/urllib#578: Both PRs refactor getDispatcherPoolStats and pool-stat extraction logic in src/HttpClient.ts and src/fetch.ts.
  • node-modules/urllib#518: This PR directly extends the initial allowH2 HTTP/2 support introduced in #518 with per-request negotiation and HttpAgent fixes.
  • node-modules/urllib#561: Both PRs touch the rejectUnauthorized/allowH2 combination in src/index.ts default-client wiring.

Suggested labels

enhancement

Suggested reviewers

  • killagu
  • akitaSummer

🐇 Hop hop, undici is eight,
allowH2 picks its fate per request!
Pool stats now merge clean and straight,
The Map-keyed cache handles the rest.
Node twenty-two-nineteen, don't be late —
This rabbit approves, quite impressed! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: upgrade to undici@8' clearly and specifically describes the main change in the pull request—upgrading the undici dependency to version 8.
Linked Issues check ✅ Passed The PR successfully addresses issue #765 by upgrading undici from v7 to v8 and setting Node.js minimum version to 22.19.0, with comprehensive implementation of HTTP/2 negotiation controls.
Out of Scope Changes check ✅ Passed All changes are directly related to the undici@8 upgrade objectives: dependency updates, Node.js version enforcement, HTTP/2 negotiation support, pool stats normalization, and related test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/upgrade-undici-8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@socket-security

socket-security Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedform-data@​4.0.69910010096100
Updatedundici@​7.27.2 ⏵ 8.4.197100100 +198100

View full report

@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.91%. Comparing base (b2b4164) to head (a9a0b2b).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #820      +/-   ##
==========================================
+ Coverage   94.58%   94.91%   +0.32%     
==========================================
  Files          10       10              
  Lines         739      747       +8     
  Branches      231      235       +4     
==========================================
+ Hits          699      709      +10     
+ Misses         37       35       -2     
  Partials        3        3              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request upgrades undici to version 8.4.1 and updates the Node engine requirement to >= 22.19.0. It also updates type definitions for AbortSignal, adjusts test assertions to align with undici@8's default HTTP/2 multiplexing and error reporting behavior, and ensures proxy agents are properly closed in tests. The review comments highlight critical issues where explicitly setting allowH2: false is ignored because the codebase only checks for truthiness when configuring the dispatcher or caching client instances. Consequently, clients fall back to the default undici@8 behavior, which enables HTTP/2 by default. Additionally, it is recommended to add assertions in the tests to verify that HTTP/2 is indeed disabled when allowH2 is set to false.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/HttpClient.ts Outdated
Comment thread src/index.ts Outdated
Comment thread test/HttpClient.test.ts
Comment on lines +124 to 127
// undici@8 negotiates HTTP/2 with h2-capable servers, so all requests are
// multiplexed over a single connection instead of opening one per request.
assert.equal(httpClient.getDispatcherPoolStats()['https://registry.npmmirror.com'].connected, 1);
assert(httpClient.getDispatcherPoolStats()[_url.substring(0, _url.length - 1)].connected > 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The test should work with allowH2 = true creates httpClient1 with allowH2: false but never asserts that HTTP/2 is actually disabled for it (it only asserts response.status === 200).

Because of the bug in HttpClient's constructor and getDefaultHttpClient where allowH2: false is ignored, httpClient1 is actually still using HTTP/2 under the hood.

We should add an assertion to verify that httpClient1 indeed uses HTTP/1.1 (e.g., by checking that it does not multiplex requests or by checking the socket/protocol if available) to prevent this regression.

fengmk2 added 2 commits June 14, 2026 22:15
Cover undici@8 default behavior with local TLS servers: HTTP/1.1 fallback
when the server has no HTTP/2 support, and HTTP/2 negotiation when it does.
vp config no longer tracks the vite-plus MCP server settings or skill
symlink under .claude/, so remove the committed files.
@pkg-pr-new

pkg-pr-new Bot commented Jun 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/node-modules/urllib@820

commit: a9a0b2b

Since undici@8 defaults allowH2 to true, an explicit allowH2: false was
silently ignored and HTTP/2 was still negotiated. The HttpClient constructor
now pins a dedicated dispatcher whenever allowH2 is set (true or false), and
getDefaultHttpClient caches separate clients for the allowH2: false case so
request(url, { allowH2: false }) forces HTTP/1.1.

Addresses review feedback on #820.
@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

Addressed in b6d10ab.

The three comments share one root cause: undici@8 defaults allowH2 to true, so an explicit allowH2: false was silently ignored.

  • HttpClient constructor: now pins a dedicated dispatcher whenever allowH2 is set explicitly (!== undefined), so allowH2: false forces HTTP/1.1 instead of falling back to the HTTP/2-enabled global dispatcher.
  • getDefaultHttpClient: caches separate clients for the allowH2: false case (with and without rejectUnauthorized), so request(url, { allowH2: false }) forces HTTP/1.1.
  • Tests: added deterministic local-server coverage (protocol negotiation): HTTP/1.1 fallback, HTTP/2-by-default, allowH2: false forcing HTTP/1.1 against an h2-capable server, plus regression checks that explicit allowH2 pins a dedicated dispatcher and that the default clients are cached per allowH2 value.

This keeps undici@8's HTTP/2 default (unset allowH2) while making false a working opt-out.

Keep patch coverage strict while allowing a 1% project threshold, so
external-network test jitter and undici@8 protocol-path changes (an h2
error branch now reachable only via upgrade streams) don't fail CI.
@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2dfc9a3e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread package.json
"qs": "^6.15.0",
"type-fest": "^4.41.0",
"undici": "^7.24.0",
"undici": "^8.4.1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor allowH2:false in FetchFactory options

With this upgrade, omitting allowH2 now means HTTP/2 is enabled by default, but the same ClientOptions type is also used by FetchFactory.setClientOptions(): when callers pass only { allowH2: false }, src/fetch.ts still checks clientOptions?.allowH2 truthily and constructs a BaseAgent without the option, so undici v8 negotiates HTTP/2 anyway. This leaves fetch users without the documented HTTP/1.1 opt-out unless they also provide connect/lookup or a custom dispatcher.

Useful? React with 👍 / 👎.

fetch shared the same truthy-check bug: passing only { allowH2: false } left
the BaseAgent without the option, so undici@8 still negotiated HTTP/2. Pin the
dispatcher whenever allowH2 is set explicitly, matching HttpClient.

Addresses Codex review feedback on #820.
@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65ac925df4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/HttpClient.ts
// Pin the protocol when allowH2 is set explicitly: `true` enables HTTP/2,
// `false` forces HTTP/1.1 instead of following undici@8's HTTP/2 default.
this.#dispatcher = new Agent({
allowH2: clientOptions.allowH2,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize stats keys for HTTP/1-only agents

Passing allowH2: false here makes undici v8 store the internal client under a key like https://host#http1-only; getDispatcherPoolStats() exposes those internal kClients keys as documented origins, so callers using new HttpClient({ allowH2: false }) or request(..., { allowH2: false }) can no longer look up stats by the origin URL and get undefined. Consider normalizing that suffix (or using the dispatcher origin) when building the stats map.

Useful? React with 👍 / 👎.

Comment thread src/index.ts Outdated
Comment on lines +60 to +64
if (allowH2 === false) {
if (!disallowH2HttpClient) {
disallowH2HttpClient = new HttpClient({
allowH2,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve global dispatchers when disabling H2

In applications that install a global dispatcher with setGlobalDispatcher() (for example a ProxyAgent or MockAgent), request(url, { allowH2: false }) now takes this new branch and creates urllib's own Agent, so the request no longer goes through the configured global proxy/mock; before this change the falsy option fell through to the default client/global dispatcher. Consider applying the HTTP/1-only choice without bypassing the active dispatcher, or requiring callers to pass an explicit dispatcher for this combination.

Useful? React with 👍 / 👎.

Addresses Codex review feedback on #820:

- allowH2: false no longer creates urllib's own Agent (which bypassed a
  user-installed global ProxyAgent/dispatcher). It is now forwarded as a
  per-request undici option, so HTTP/1.1 is forced through the active
  dispatcher. Only allowH2: true still uses a dedicated agent.
- getDispatcherPoolStats() normalizes undici@8's `${origin}#http1-only`
  pool keys back to the origin so stats remain reachable by URL.
@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14acd12754

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/index.ts Outdated
Comment thread src/HttpClient.ts Outdated
Comment thread src/HttpClient.ts
- Skip per-request allowH2 forwarding when the active dispatcher is a
  MockAgent: undici keys http1-only clients as `${origin}#http1-only`, which
  would miss interceptors registered on the plain origin (protocol choice is
  moot when mocking).
- normalizePoolStatsKey tolerates non-string client keys (MockAgent RegExp /
  function origin matchers) instead of throwing.
- getDefaultHttpClient caches a distinct allowH2: false client again so
  getDefaultHttpClient(undefined, false).request(url) forces HTTP/1.1 without a
  per-request flag, while still using the global dispatcher.
@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8c1c98aac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/HttpClient.ts Outdated
Comment thread src/HttpClient.ts Outdated
Addresses Codex review feedback on #820:

- HttpAgent no longer bakes allowH2 into its `connect` options. undici builds
  the connector as `buildConnector({ allowH2, ...connect })`, so a connect-level
  allowH2 shadowed the top-level/per-request value; keeping it top-level lets
  `request(..., { allowH2: false })` force HTTP/1.1 for lookup/checkAddress
  clients too.
- mergePoolStat coerces missing counters to 0, so undici ClientStats (Agent with
  connections: 1, no free/queued) no longer produce NaN in getDispatcherPoolStats().
@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44ef14ac03

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/HttpClient.ts Outdated
Comment thread src/HttpClient.ts Outdated
// the plain origin (protocol negotiation is moot when mocking anyway).
const activeDispatcher = requestOptions.dispatcher ?? getGlobalDispatcher();
if (!(activeDispatcher instanceof MockAgent)) {
requestOptions.allowH2 = allowH2;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle allowH2:false for Pool dispatchers

When callers pass a Pool/Client as options.dispatcher, setting requestOptions.allowH2 here does not force HTTP/1.1: undici only uses the dispatch-level flag in Agent to choose a separate #http1-only client, while a Pool or Client has already built its connector from constructor options. In that scenario, request(url, { dispatcher: new Pool(origin), allowH2: false }) still negotiates HTTP/2 against an h2-capable server under undici@8 despite the new per-request option, so this combination needs special handling or should be rejected/documented as unsupported.

Useful? React with 👍 / 👎.

Clarify on the allowH2 request option that the per-request HTTP/1.1 opt-out is
honored by Agent-based dispatchers (default agent, ProxyAgent) but cannot
downgrade a raw Pool/Client built for HTTP/2, and is not applied to MockAgent.

Addresses Codex review feedback on #820.
@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

Addressed in 3d04056.

Both points are inherent undici@8 constraints (the per-request allowH2 flag is only fully honored by Agent), so they're now documented on the allowH2 request option rather than worked around:

  • Pool/Client dispatcher: a raw Pool/Client builds its connector at construction, so it can't be downgraded per request. Documented that callers should construct it with allowH2: false instead (the default agent and ProxyAgent, both Agent-based, are honored).
  • MockAgent passthrough: there's no pre-dispatch API to know whether a request will match an interceptor or pass through, and keeping the flag re-introduces the #http1-only pool split that breaks interceptor matching (the common case). Documented that allowH2 is not applied under MockAgent, so passthrough follows the mock agent's own default.

The well-supported path (request(url, { allowH2: false }) through the default/global agent or a ProxyAgent) forces HTTP/1.1 and is covered by tests.

@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 3d0405675f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

Exercise getDefaultHttpClient(false, false/true/undefined) so the cached
allowH2: false client for rejectUnauthorized is covered by tests.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 3d0405675f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@fengmk2

fengmk2 commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 9cf39ef3d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fengmk2 added 4 commits June 15, 2026 09:12
- mergePoolStat: add a full all-counter sum case (previously only the
  zero-coercion path asserted, with a partial second stat).
- getDispatcherPoolStats: drive a real collision by issuing one HTTP/2 and one
  allowH2: false HTTP/1.1 request to the same origin on one dispatcher, then
  assert the `${origin}` and `${origin}#http1-only` pools collapse into a single
  origin entry whose connected count is the sum (2), not an overwrite.
Instead of a single request per protocol, issue several concurrent HTTP/2 and
allowH2: false HTTP/1.1 requests so both the `${origin}` and `${origin}#http1-only`
pools hold multiple connections. Verify the merged getDispatcherPoolStats() entry
equals the field-by-field sum of the two raw undici pools (observed e.g. 5 + 8 = 13
connected), proving a real sum rather than an overwrite.
Code-review cleanups (no behavior change to the public API):
- Extract the duplicated getDispatcherPoolStats iteration into a shared
  buildPoolStats(agent) used by both HttpClient and FetchFactory; drop the now
  unused Pool/undici symbols imports from fetch.ts.
- Hoist the `#http1-only` pool-key suffix into a constant and simplify
  mergePoolStat to a single summing expression.
- Harden the allowH2 MockAgent skip with a constructor-name fallback so a
  MockAgent from a duplicate undici install is still detected.
- Fix the getDefaultHttpClient comment: rejectUnauthorized + allowH2:false
  clients do own a dispatcher (to carry the TLS option).
…ures

Quality cleanups from /simplify (no public behavior change):
- getDefaultHttpClient: replace the 6 module-level singletons + nested branch
  ladder with one Map keyed by (rejectUnauthorized, allowH2).
- Extract an isMockAgent() predicate and flatten the per-request allowH2 guard.
- Drop the redundant allowH2 redeclaration on UrllibRequestOptions (inherited
  from RequestOptions) and the single-use AbortSignal type alias.
- Tests: share a startH2EchoServer() fixture across the protocol-negotiation /
  pool-stats tests instead of repeating the same secure-server boilerplate.
@fengmk2 fengmk2 marked this pull request as ready for review June 15, 2026 14:23
Copilot AI review requested due to automatic review settings June 15, 2026 14:23

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

This PR upgrades undici to v8, raises the minimum supported Node.js version to match undici’s engine requirement, and adapts urllib’s dispatcher/HTTP protocol behavior to undici@8’s default HTTP/2 negotiation (allowH2 now defaults to true).

Changes:

  • Upgrade undici to ^8.4.1 and bump engines.node to >= 22.19.0 (plus lockfile updates).
  • Update HttpClient/FetchFactory behavior and types to support per-request allowH2 handling and to normalize/merge pool stats with undici@8’s #http1-only pool keys.
  • Refresh CI/test expectations for undici@8 (HTTP/2 multiplexing, timeout error types/messages, teardown cleanup), add Codecov config, and update CI Node matrix.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
package.json Bumps undici dependency and Node engine requirement.
pnpm-lock.yaml Lockfile updates for undici@8 and related dependency bumps.
.github/workflows/nodejs.yml Updates CI test matrix to Node 22/24/26.
codecov.yml Adds Codecov thresholds (strict patch coverage, tolerant project coverage).
src/Request.ts Adds/updates allowH2 option docs and tightens signal typing for undici@8.
src/index.ts Refactors default HttpClient caching to include allowH2 combinations.
src/HttpClient.ts Implements per-request allowH2, MockAgent handling, and normalized/merged pool stats helpers.
src/HttpAgent.ts Adjusts connector option shaping to keep allowH2 top-level (to support per-request overrides).
src/fetch.ts Pins allowH2 when explicitly set; reuses shared pool stats builder.
test/options.timeout.test.ts Updates expected HTTP/2 timeout error type/code/message for undici@8.
test/options.dispatcher.test.ts Ensures ProxyAgent instances are closed in teardown to avoid shutdown hangs.
test/index.test.ts Adds regression test ensuring MockAgent interceptors aren’t bypassed by allowH2: false.
test/HttpClient.test.ts Adds ALPN negotiation + pool-stats merge tests and updates HTTP/2 multiplexing expectations.
test/fetch.test.ts Adds test ensuring setClientOptions({ allowH2: false }) is honored.
.claude/settings.json Removes Claude MCP settings file from repo.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/HttpAgent.ts
Comment on lines +69 to +72
// Keep allowH2 at the top level only. undici builds the connector as
// `buildConnector({ allowH2, ...connect })`, so an allowH2 inside `connect`
// would shadow the top-level/per-request value and defeat `allowH2: false`.
connect: { ...options.connect, lookup: lookupFunction },
…test fixtures

From /code-review:
- normalizePoolStatsKey strips only a trailing `#http1-only` suffix (endsWith)
  instead of the first indexOf occurrence.
- getDefaultHttpClient cache key is self-descriptive
  (`rejectUnauthorized=...:allowH2=...`) instead of ternary-of-ternaries ints.
- Share startEchoServer() so the HTTP/1.1-only test reuses the fixture too, and
  the fixture owns teardown (returns close()), removing hand-rolled server.close
  promises from every protocol test.
- Add missing teardown to the `should timeout on h2` test (server + dispatcher).
- Fix the merge test's inaccurate "multiplex over a single connection" comment
  and drop a tautological NaN assertion in the mergePoolStat unit test.
@fengmk2 fengmk2 merged commit d94609b into master Jun 15, 2026
19 checks passed
@fengmk2 fengmk2 deleted the fix/upgrade-undici-8 branch June 15, 2026 14:55
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.

upgrade to undici@8

2 participants