Skip to content

Migrate from got to undici - #321

Open
diemol wants to merge 3 commits into
mainfrom
migrate-got-to-undici
Open

Migrate from got to undici#321
diemol wants to merge 3 commits into
mainfrom
migrate-got-to-undici

Conversation

@diemol

@diemol diemol commented Aug 4, 2026

Copy link
Copy Markdown
Member

Migrate from got to undici

Issue: related to #319 (dependabot bump of got 11.8.6 → 15.1.0)

Description

got@15 is pure ESM-only and requires Node >=22, which breaks the CJS build
(Jest can't require() an ESM-only package). Rather than take that breaking
bump or paper over it with a dynamic import() workaround, this PR replaces
got entirely with the latest undici (Node's own HTTP client, published
as both CJS and ESM, maintained by the Node core team, so this ESM-only
problem can't recur).

Latest undici (8.x) itself requires Node >=22.19.0, so this PR also raises
this package's own engines.node to >=22.19.0 (and updates the README,
Babel compile target, and CI matrix to match) rather than pin to an older
undici line just to preserve a lower Node floor.

got was used in exactly one file (src/index.js) for all Sauce Labs REST
API traffic. The response/error shape it produced (including the exact
Response code ${statusCode} (${statusText}) error message format) is
preserved byte-for-byte, since that message format is a de facto public
contract pinned by e2e/jobs.test.js.

Types of Changes

What types of changes does your code introduce? Keep the ones that apply:

  • Configuration change
  • Refactor/improvements

Tasks

List of tasks you will do to complete the PR

  • Replace createProxyAgent (src/utils.js) with an undici.ProxyAgent
    instead of the tunnel-based agent
  • Replace got.extend() client construction (src/index.js) with a
    per-instance undici dispatcher (Agent/ProxyAgent, with redirect
    + retry interceptors composed in)
  • Add a _request() wrapper replicating got's response shape and its
    throw-on-non-2xx-with-parsed-body behavior
  • Swap got/tunnel deps for latest undici (8.x), bump
    engines.node to >=22.19.0 and Babel's Node compile target to
    match, update the CI matrix (drop 20.x, now unsupported)
  • Replace the shallow tests/__mocks__/got.js module mock with
    undici.MockAgent for real HTTP-semantics test mocking
  • Drop .unmock('got') from e2e tests
  • Verify against the live Sauce Labs API via e2e tests

Review

List of tasks the reviewer must do to review the PR

  • Tests
  • Documentation
  • CHANGELOG

README's Node version line was updated to match the new floor; no other
public API changed, so no further doc updates were needed. This repo
doesn't maintain a CHANGELOG.md.

Deployment Notes

  • Raises the minimum supported Node version to 22.19.0 (up from the
    previously-documented 18+). This is a breaking change for consumers on
    older Node versions — driven by undici's own engine requirement, the
    same category of constraint that made staying on got@15 a non-starter.
  • CI matrix updated to [22.x, 24.x] (dropped 20.x, no longer supported).
  • No database migrations or feature toggles involved.

diemol and others added 3 commits August 4, 2026 06:24
got@15 is pure ESM-only and requires Node >=22, which breaks the CJS
build and would force every consumer onto a much higher Node floor
than necessary. Replace got entirely with undici (ships both CJS and
ESM, requires only Node >=18, maintained by the Node core team) rather
than take that bump or work around it with dynamic import().

- src/utils.js: createProxyAgent now returns an undici.ProxyAgent
  (with redirect + retry interceptors) instead of a tunnel-based agent
- src/index.js: replace got.extend() with a per-instance undici
  dispatcher and a new _request() wrapper that replicates got's
  response shape and its throw-on-non-2xx-with-parsed-body behavior
- package.json/babel.config.js: swap got+tunnel deps for undici, add
  engines.node >=18, bump Babel's Node target to 18
- tests: replace the shallow tests/__mocks__/got.js with undici's
  MockAgent for real HTTP-semantics mocking; drop .unmock('got') from
  e2e tests
- eslint.config.js: extend jest globals to e2e/**/*.js too (pre-commit
  lint-staged runs on all staged *.js files, and this gap had simply
  never been exercised since nobody had touched e2e/ files since the
  flat-config migration)

Verified against the live Sauce Labs API via e2e tests, including the
exact error-message contract pinned by e2e/jobs.test.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
undici@7.x actually requires Node >=20.18.1 and 8.x requires
Node >=22.19.0 - both silently contradicted the engines.node >=18
this branch previously declared. Rather than pin to an older undici
line to preserve a Node 18 floor, use the latest undici and update
engines.node/README/babel target/CI matrix to accurately reflect the
real Node >=22.19.0 requirement.

Also make the createProxyAgent test assert `instanceof Dispatcher`
(the stable base class) instead of `instanceof ProxyAgent`, since
undici's `.compose()` doesn't preserve the ProxyAgent prototype chain
consistently across all its own major versions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The lockfile conflicts from rebasing onto main's dependabot bumps were
resolved with a placeholder during the rebase; this replaces it with
a clean `npm install` regeneration from the resolved package.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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 migrates the Sauce Labs API client from got/tunnel to undici to avoid ESM-only got@15 and align with Node’s supported HTTP client stack, while raising the package’s Node engine floor to >=22.19.0 and updating tests/CI accordingly.

Changes:

  • Replaces got usage with an undici-backed dispatcher and a _request() wrapper that emulates the prior response/error shape.
  • Switches proxy support from tunnel agents to undici.ProxyAgent and updates unit tests to use undici.MockAgent.
  • Updates Node engine requirement, Babel target, CI matrix, and related docs/config.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/index.js Introduces _dispatcher + _request() wrapper and swaps API calls from got to undici.request().
src/utils.js Reimplements proxy handling via undici.ProxyAgent and removes tunnel usage.
tests/index.test.js Reworks HTTP mocking to use undici.MockAgent instead of a got module mock.
tests/utils.test.js Updates proxy agent tests to assert dispatcher instances rather than tunnel agent shapes.
tests/__snapshots__/index.test.js.snap Removes snapshots that were asserting got call shapes.
tests/__mocks__/got.js Removes the got Jest mock (no longer used).
tests/__mocks__/form-data.js Adds getHeaders() mocking for multipart upload requests.
e2e/sc.test.js Drops .unmock('got') since got is removed.
e2e/jobs.test.js Drops .unmock('got') since got is removed.
package.json Adds engines.node >=22.19.0, replaces got/tunnel with undici.
package-lock.json Updates lockfile to reflect dependency swap and Node engine constraint.
README.md Updates documented minimum Node version.
babel.config.js Updates Babel compile target to Node 22.
.github/workflows/test.yml Updates CI matrix to [22.x, 24.x].
eslint.config.js Applies Jest globals config to e2e/**/*.js as well as tests/**/*.js.

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

Comment thread src/utils.js
Comment on lines +151 to 168
* get an undici ProxyAgent for tunneling requests through a proxy
* @param {string} proxy proxy URL that traffic will be tunneled with
* @return {Agent} proxy Agent object
* @return {ProxyAgent} proxy dispatcher
*/
export function createProxyAgent(proxy) {
var proxyURL = url.parse(proxy);
if (proxyURL.protocol === 'https:') {
return {
https: tunnel.httpsOverHttps({
proxy: {
host: proxyURL.hostname,
port: proxyURL.port,
},
}),
};
} else if (proxyURL.protocol === 'http:') {
return {
https: tunnel.httpsOverHttp({
proxy: {
host: proxyURL.hostname,
port: proxyURL.port,
},
}),
};
const proxyURL = new URL(proxy);
if (proxyURL.protocol !== 'http:' && proxyURL.protocol !== 'https:') {
throw new Error(
'Only http and https protocols are supported for proxying traffic.' +
`\nWe got ${proxyURL.protocol}`
);
}

throw new Error(
'Only http and https protocols are supported for proxying traffic.' +
`\nWe got ${proxyURL.protocol}`
);
return new ProxyAgent({
uri: proxy,
requestTls: {rejectUnauthorized: getStrictSsl()},
}).compose(interceptors.redirect({maxRedirections: 5}), interceptors.retry());
}
Comment thread src/index.js
Comment on lines +456 to +463
let responseBody;
if (responseType === 'buffer') {
responseBody = Buffer.from(await res.body.arrayBuffer());
} else if (responseType === 'json') {
responseBody = await res.body.json();
} else {
responseBody = await res.body.text();
}
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