Migrate from got to undici - #321
Open
diemol wants to merge 3 commits into
Open
Conversation
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>
diemol
force-pushed
the
migrate-got-to-undici
branch
from
August 4, 2026 11:26
2db5683 to
6e68ffb
Compare
There was a problem hiding this comment.
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
gotusage with anundici-backed dispatcher and a_request()wrapper that emulates the prior response/error shape. - Switches proxy support from
tunnelagents toundici.ProxyAgentand updates unit tests to useundici.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 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 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(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Migrate from got to undici
Description
got@15is pure ESM-only and requires Node >=22, which breaks the CJS build(Jest can't
require()an ESM-only package). Rather than take that breakingbump or paper over it with a dynamic
import()workaround, this PR replacesgotentirely with the latestundici(Node's own HTTP client, publishedas 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 raisesthis package's own
engines.nodeto>=22.19.0(and updates the README,Babel compile target, and CI matrix to match) rather than pin to an older
undiciline just to preserve a lower Node floor.gotwas used in exactly one file (src/index.js) for all Sauce Labs RESTAPI traffic. The response/error shape it produced (including the exact
Response code ${statusCode} (${statusText})error message format) ispreserved 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:
Tasks
List of tasks you will do to complete the PR
createProxyAgent(src/utils.js) with anundici.ProxyAgentinstead of the
tunnel-based agentgot.extend()client construction (src/index.js) with aper-instance
undicidispatcher (Agent/ProxyAgent, with redirect+ retry interceptors composed in)
_request()wrapper replicating got's response shape and itsthrow-on-non-2xx-with-parsed-body behavior
got/tunneldeps for latestundici(8.x), bumpengines.nodeto>=22.19.0and Babel's Node compile target tomatch, update the CI matrix (drop
20.x, now unsupported)tests/__mocks__/got.jsmodule mock withundici.MockAgentfor real HTTP-semantics test mocking.unmock('got')from e2e testsReview
List of tasks the reviewer must do to review the PR
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
previously-documented 18+). This is a breaking change for consumers on
older Node versions — driven by
undici's own engine requirement, thesame category of constraint that made staying on
got@15a non-starter.[22.x, 24.x](dropped20.x, no longer supported).