Skip to content

feat!: drop Node.js < 22 support#722

Merged
fengmk2 merged 1 commit intomasterfrom
drop-v20-support
Mar 30, 2026
Merged

feat!: drop Node.js < 22 support#722
fengmk2 merged 1 commit intomasterfrom
drop-v20-support

Conversation

@fengmk2
Copy link
Copy Markdown
Member

@fengmk2 fengmk2 commented Mar 29, 2026

Summary

  • BREAKING CHANGE: Minimum supported Node.js version is now 22.0.0
  • Remove patchForNode16() polyfills from production code (src/utils.ts, src/index.ts)
  • Delete old Node.js CI workflow (.github/workflows/nodejs-old-versions.yml) and test patches (test/patch-for-node16-18.cjs, test/setup.ts)
  • Remove polyfill devDependencies (@ungap/structured-clone, string.prototype.towellformed)
  • Simplify version-conditional test code across 10 test files (keySize ternaries, skipIf checks, version-branched assertions)
  • Update CI matrix: Node.js 22, 24, 25 only

Test plan

  • vp check passes (lint, format)
  • vp test run passes (40 test files, 202 tests)

Closes #718

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Updated minimum Node.js version requirement to 22.0.0
    • Removed unused development dependencies
    • Simplified runtime initialization and test infrastructure
    • Updated CI workflows to test Node.js 22, 24, and 25

BREAKING CHANGE: Minimum supported Node.js version is now 22.0.0.

- Remove patchForNode16() polyfills from src/utils.ts and src/index.ts
- Remove old Node.js CI workflow and test patches
- Remove polyfill devDependencies (@ungap/structured-clone, string.prototype.towellformed)
- Simplify version-conditional test code (keySize, skipIf, assertions)
- Update CI matrix to test Node.js 22, 24, 25 only

Closes #718
Copilot AI review requested due to automatic review settings March 29, 2026 13:47
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

This PR removes Node.js 16 and 18 support, establishing Node.js 22 as the minimum required version. Changes include deleting deprecated CI workflows, removing runtime polyfills and patches, eliminating version-conditional test logic, and simplifying TLS key generation across tests.

Changes

Cohort / File(s) Summary
CI Workflow Updates
.github/workflows/nodejs-old-versions.yml, .github/workflows/nodejs.yml
Deleted Node.js 16/18 CI workflow and removed Node.js 20 from the remaining matrix, leaving only versions 22, 24, and 25.
Package Configuration
package.json
Removed test-node16 script, simplified prepare script, dropped @ungap/structured-clone and string.prototype.towellformed dependencies, and raised minimum Node engine requirement to >= 22.0.0.
Runtime Compatibility Removal
src/utils.ts, src/index.ts
Deleted patchForNode16() function and all Node 16-specific polyfills (including ReadableStream, TransformStream, Blob, DOMException, File), and removed its invocation from module initialization.
Source Code Documentation
src/HttpAgent.ts
Updated inline comments to reflect current behavior without Node.js version specifics.
Test Setup & Infrastructure
test/setup.ts, test/utils.ts, test/patch-for-node16-18.cjs, vite.config.ts
Removed patchForNode16 invocation from test setup, deleted nodeMajorVersion() utility function, deleted runtime patch file, and removed setupFiles configuration from Vite.
Test Suite Simplifications
test/HttpClient.test.ts, test/diagnostics_channel.test.ts, test/fixtures/server.ts, test/options.compressed.test.ts, test/options.dataType.test.ts, test/options.fixJSONCtlChars.test.ts, test/options.timeout.test.ts, test/options.writeStream.test.ts, test/urllib.options.rejectUnauthorized-false.test.ts
Removed Node.js version-conditional test logic, fixed TLS key sizes to 2048, removed conditional test skips, eliminated version-dependent error message expectations, and simplified server connection cleanup.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Drop Node.js < 22 support #718: Directly implements dropping Node.js < 22 support through engine requirement bump, removal of polyfills, CI workflow updates, and test simplifications.

Possibly related PRs

  • test: use vitest@3 #570: Related through overlapping changes to test tooling, package.json Vitest configuration, and removal of Node 16/18 polyfill dependencies.
  • test: run test on Node.js 23 #543: Related through CI Node-version matrix modifications and removal of nodeMajorVersion utility usage in test files.
  • chore: enable oxfmt #602: Related through modifications to the patchForNode16 function in src/utils.ts and its surrounding compatibility code.

Suggested labels

enhancement

Poem

🐰 No more patches for those old times,
Node twenty-two now draws the lines,
Version checks stripped clean away,
Simpler tests can now just play,
Hop ahead to modern day! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat!: drop Node.js < 22 support' accurately summarizes the main change: dropping support for Node.js versions below 22, which aligns with the entire changeset.
Linked Issues check ✅ Passed The pull request comprehensively implements the objective to drop Node.js < 22 support by removing polyfills, updating the minimum engine requirement to 22.0.0, removing Node 16/18 CI workflows and test patches, and simplifying version-conditional code.
Out of Scope Changes check ✅ Passed All changes are directly scoped to dropping Node.js < 22 support; no unrelated modifications are present in the changeset.

✏️ 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 drop-v20-support

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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 29, 2026

Open in StackBlitz

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

commit: 9f26c9e

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 29, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.60%. Comparing base (46c6ac9) to head (9f26c9e).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #722      +/-   ##
==========================================
+ Coverage   92.77%   94.60%   +1.83%     
==========================================
  Files          10       10              
  Lines         747      723      -24     
  Branches      233      226       -7     
==========================================
- Hits          693      684       -9     
+ Misses         51       36      -15     
  Partials        3        3              

☔ View full report in Codecov by Sentry.
📢 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.

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 minimum required Node.js version to 22.0.0 and removes legacy compatibility code, including the patchForNode16 utility, version-specific test skips, and polyfill dependencies like @ungap/structured-clone. Test configurations and server fixtures have been simplified to utilize modern Node.js APIs directly. I have no feedback to provide.

Copy link
Copy Markdown

Copilot AI left a comment

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 updates the project’s runtime baseline to Node.js >= 22, removing legacy polyfills and simplifying tests/CI that previously supported older Node versions.

Changes:

  • Raise the minimum supported Node.js version to >= 22.0.0 and remove Node 20/18/16 compatibility code.
  • Remove Node 16-era polyfill patching from runtime and test setup, and delete the old-versions CI workflow.
  • Simplify version-conditional test logic and drop polyfill-only devDependencies.

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated no comments.

Show a summary per file
File Description
vite.config.ts Removes Vitest setupFiles now that legacy runtime patching is no longer needed.
test/utils.ts Removes nodeMajorVersion() helper used for old-version branching.
test/urllib.options.rejectUnauthorized-false.test.ts Removes Node-version keysize conditional; fixes key size at 2048.
test/setup.ts Deletes Node 16 patch bootstrap for tests.
test/patch-for-node16-18.cjs Deletes polyfills needed only for Node 16/18 test runs.
test/options.writeStream.test.ts Removes Node 16 skip and legacy comments tied to old stream behavior.
test/options.timeout.test.ts Removes Node-version keysize conditional; fixes key size at 2048.
test/options.fixJSONCtlChars.test.ts Removes Node-version-specific comment; keeps assertion general.
test/options.dataType.test.ts Removes Node-version branching in JSON parse error assertions.
test/options.compressed.test.ts Removes Node-version branching for brotli error code assertion.
test/fixtures/server.ts Removes Node 16 connection-tracking fallback; uses closeAllConnections() directly; fixes HTTPS key size at 2048.
test/diagnostics_channel.test.ts Removes Node-version keysize conditional; fixes key size at 2048.
test/HttpClient.test.ts Removes Node 16 skips and lookup branching tied to older Node DNS behavior.
src/utils.ts Removes patchForNode16() and related imports/polyfill logic from production code.
src/index.ts Stops invoking patchForNode16() at module init.
src/HttpAgent.ts Updates comments related to DNS lookup behavior under newer Node versions.
pnpm-lock.yaml Removes polyfill-related dependencies from the lockfile.
package.json Bumps engines.node to >= 22.0.0, removes polyfill devDeps, and simplifies prepare.
.github/workflows/nodejs.yml Updates CI Node matrix to 22/24/25.
.github/workflows/nodejs-old-versions.yml Deletes old Node 16/18 CI workflow.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/HttpClient.test.ts (1)

128-183: ⚠️ Potential issue | 🟠 Major

Close the HTTP/2 server in this test to prevent resource leaks.

The test creates a server at line 151 but never closes it, leaving an open handle. This can cause test flakiness and resource leaks. Add server.close() in a finally block to ensure cleanup.

Suggested fix
      server.listen(0);
      await once(server, 'listening');
-
+    try {
       const httpClient = new HttpClient({
         allowH2: true,
         connect: {
           rejectUnauthorized: false,
         },
       });

       const url = `https://localhost:${(server.address() as AddressInfo).port}`;
       let response = await httpClient.request<string>(url, {
         dataType: 'text',
         headers: {
           'x-my-header': 'foo',
         },
       });
       assert.equal(response.status, 200);
       assert.equal(response.headers['x-custom-h2'], 'hello');
       assert.equal(response.data, 'hello h2!');
       await sleep(200);
       response = await httpClient.request<string>(url, {
         dataType: 'text',
         headers: {
           'x-my-header': 'foo2',
         },
       });
       assert.equal(response.status, 200);
       assert.equal(response.headers['x-custom-h2'], 'hello');
       assert.equal(response.data, 'hello h2!');
+    } finally {
+      await new Promise<void>((resolve, reject) => {
+        server.close((err?: Error) => (err ? reject(err) : resolve()));
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/HttpClient.test.ts` around lines 128 - 183, The test "should not exit
after other side closed error" creates a server (variable server) but never
closes it; wrap the request logic in a try/finally so you always call
server.close() in the finally block (and optionally await the 'close' event with
once(server, 'close')) to ensure the HTTP/2 server is cleaned up and no open
handles remain; keep the existing assertions and HttpClient.request usage as-is
and only add the finally cleanup around them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/HttpAgent.ts`:
- Around line 44-49: Update the inline comments in HttpAgent.ts to correctly
reflect dns.lookup behavior: change the comment near the args destructuring
(refs: args[0] -> address, args[1] -> family) to say "address will be a string
when dnsOptions.all defaults to false" and update the later comment inside the
options.checkAddress block to "dnsOptions.all defaults to false, dns.lookup
returns a single address string"; keep the surrounding logic unchanged
(references: address, family, args, options.checkAddress, dns.lookup,
dnsOptions.all).

In `@test/fixtures/server.ts`:
- Around line 441-445: The shutdown order in closeServer() is inverted; call
server.close(callback) first to stop accepting new connections, then call
server.closeAllConnections() to terminate existing ones. Update the
closeServer() function so it invokes server.close(resolve) (or wraps
server.close in a Promise) before calling server.closeAllConnections(),
referencing the existing closeServer, server.close, and
server.closeAllConnections symbols.

---

Outside diff comments:
In `@test/HttpClient.test.ts`:
- Around line 128-183: The test "should not exit after other side closed error"
creates a server (variable server) but never closes it; wrap the request logic
in a try/finally so you always call server.close() in the finally block (and
optionally await the 'close' event with once(server, 'close')) to ensure the
HTTP/2 server is cleaned up and no open handles remain; keep the existing
assertions and HttpClient.request usage as-is and only add the finally cleanup
around them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cee608f9-d2cd-46c6-9857-21970ac9d4cd

📥 Commits

Reviewing files that changed from the base of the PR and between 46c6ac9 and 9f26c9e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .github/workflows/nodejs-old-versions.yml
  • .github/workflows/nodejs.yml
  • package.json
  • src/HttpAgent.ts
  • src/index.ts
  • src/utils.ts
  • test/HttpClient.test.ts
  • test/diagnostics_channel.test.ts
  • test/fixtures/server.ts
  • test/options.compressed.test.ts
  • test/options.dataType.test.ts
  • test/options.fixJSONCtlChars.test.ts
  • test/options.timeout.test.ts
  • test/options.writeStream.test.ts
  • test/patch-for-node16-18.cjs
  • test/setup.ts
  • test/urllib.options.rejectUnauthorized-false.test.ts
  • test/utils.ts
  • vite.config.ts
💤 Files with no reviewable changes (8)
  • src/index.ts
  • test/setup.ts
  • test/options.fixJSONCtlChars.test.ts
  • test/utils.ts
  • vite.config.ts
  • src/utils.ts
  • test/patch-for-node16-18.cjs
  • .github/workflows/nodejs-old-versions.yml

Comment on lines +44 to +49
// address will be array when dnsOptions.all is set to default
const address = args[0];
const family = args[1];
if (err) return (callback as any)(err, address, family);
if (options.checkAddress) {
// dnsOptions.all set to default on Node.js >= 20, dns.lookup will return address array object
// dnsOptions.all set to default, dns.lookup will return address array object
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the default value of dnsOptions.all in Node.js dns.lookup

# Test the actual behavior of dns.lookup with default options
node --eval "
const dns = require('node:dns');

console.log('Testing dns.lookup with default options (no all parameter):');
dns.lookup('example.com', {}, (err, address, family) => {
  console.log('Type of address:', typeof address);
  console.log('Is array:', Array.isArray(address));
  console.log('Address value:', address);
  console.log('Family:', family);
});

setTimeout(() => {
  console.log('\nTesting dns.lookup with all: true:');
  dns.lookup('example.com', { all: true }, (err, addresses) => {
    console.log('Type of addresses:', typeof addresses);
    console.log('Is array:', Array.isArray(addresses));
    console.log('Addresses:', addresses);
  });
}, 1000);
"

Repository: node-modules/urllib

Length of output: 478


🌐 Web query:

What is the default value of the 'all' option in Node.js dns.lookup() function? Does it default to true or false?

💡 Result:

The default value of the 'all' option in Node.js dns.lookup is false. When 'all' is false (default), the callback receives a single address; when true, it receives an array of all resolved addresses.

Citations:


Fix comments to accurately describe dns.lookup behavior.

Lines 44 and 49 incorrectly state that address will be an array when dnsOptions.all is at its default. The all option defaults to false, which means dns.lookup returns a single address string by default. The array behavior only occurs when all is explicitly set to true.

Update the comments to accurately reflect this:

  • Line 44: "address will be a string when dnsOptions.all defaults to false"
  • Line 49: "dnsOptions.all defaults to false, dns.lookup returns a single address string"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HttpAgent.ts` around lines 44 - 49, Update the inline comments in
HttpAgent.ts to correctly reflect dns.lookup behavior: change the comment near
the args destructuring (refs: args[0] -> address, args[1] -> family) to say
"address will be a string when dnsOptions.all defaults to false" and update the
later comment inside the options.checkAddress block to "dnsOptions.all defaults
to false, dns.lookup returns a single address string"; keep the surrounding
logic unchanged (references: address, family, args, options.checkAddress,
dns.lookup, dnsOptions.all).

Comment on lines 441 to 445
closeServer() {
if (hasCloseAllConnections) {
(server as any).closeAllConnections();
} else {
// console.log('Closing %d http connections', connections.length);
for (const connection of connections) {
connection.destroy();
}
}
server.closeAllConnections();
return new Promise((resolve) => {
server.close(resolve);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Node.js server.closeAllConnections() vs server.close() order shutdown

💡 Result:

For graceful shutdown in Node.js HTTP servers (v18.2.0+), first call server.close to stop accepting new connections and allow active requests to complete, then call server.closeAllConnections to forcefully close any remaining connections (including idle keep-alive ones). This ensures the server.close callback fires promptly without hanging on persistent connections. Example: process.on('SIGTERM', => { server.close((err) => { if (err) { console.error(err); process.exit(1); } console.log('Connections drained'); // Additional cleanup (DB, etc.) process.exit(0); }); server.closeAllConnections; // Force close remaining sockets }); server.close: Stops new connections, waits for all connections to naturally close after active requests finish (may hang on idle keep-alive). Callback fires when no connections remain. server.closeAllConnections: Forcefully closes/destroys all server connections (idle and active HTTP ones, not upgraded WebSockets). Use after close for non-graceful draining of lingerers. No callback; synchronous effect on connection count. For truly graceful: Use server.closeIdleConnections after close instead, to close only idle ones while letting active finish. Always set a timeout to force process.exit if shutdown hangs.

Citations:


🌐 Web query:

Node.js http.Server.close() and closeAllConnections() correct sequence

💡 Result:

The correct sequence for shutting down a Node.js http.Server using close and closeAllConnections depends on the desired behavior: graceful (allow in-flight requests to complete) vs forceful (immediate closure). For graceful shutdown (recommended for production): 1. Call server.close([callback]) - Stops accepting new connections, allows active requests to finish and idle keep-alive connections to drain naturally. 2. Optionally, after calling close, call server.closeIdleConnections (if available, v18.2.0+) to close idle keep-alive connections faster without interrupting active requests. For forceful shutdown (e.g., tests, immediate termination): 1. Call server.closeAllConnections - Force-closes ALL connections (idle and active), then 2. Call server.close([callback]) - Ensures no new connections and triggers the 'close' event/callback once drained. Example (forceful): server.close( => { console.log('Server closed'); }); server.closeAllConnections; Example (graceful): server.close( => { console.log('Server closed'); }); // Optional: server.closeIdleConnections; closeAllConnections was added in v18.2.0. Prior versions require manual socket tracking via 'connection' event. Since today's date is 2026-03-29 and current Node.js is v25+, these methods are available. Use graceful shutdown unless immediate closure is required, as closeAllConnections interrupts active requests.

Citations:


🏁 Script executed:

cat -n test/fixtures/server.ts | sed -n '438,448p'

Repository: node-modules/urllib

Length of output: 462


Swap the shutdown order in closeServer().

server.close() must be called before server.closeAllConnections(). The current code closes existing connections first, leaving the server still accepting new connections until close() is called. This creates a race condition where a connection can slip in between the two calls and cause teardown to hang unexpectedly.

🛠️ Proposed fix
         closeServer() {
-          server.closeAllConnections();
           return new Promise((resolve) => {
             server.close(resolve);
+            server.closeAllConnections();
           });
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
closeServer() {
if (hasCloseAllConnections) {
(server as any).closeAllConnections();
} else {
// console.log('Closing %d http connections', connections.length);
for (const connection of connections) {
connection.destroy();
}
}
server.closeAllConnections();
return new Promise((resolve) => {
server.close(resolve);
});
closeServer() {
return new Promise((resolve) => {
server.close(resolve);
server.closeAllConnections();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/server.ts` around lines 441 - 445, The shutdown order in
closeServer() is inverted; call server.close(callback) first to stop accepting
new connections, then call server.closeAllConnections() to terminate existing
ones. Update the closeServer() function so it invokes server.close(resolve) (or
wraps server.close in a Promise) before calling server.closeAllConnections(),
referencing the existing closeServer, server.close, and
server.closeAllConnections symbols.

Copy link
Copy Markdown
Member

@killagu killagu left a comment

Choose a reason for hiding this comment

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

LGTM

@fengmk2 fengmk2 merged commit 26d9b1e into master Mar 30, 2026
23 checks passed
@fengmk2 fengmk2 deleted the drop-v20-support branch March 30, 2026 02:51
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.

Drop Node.js < 22 support

3 participants