On Node.js 26, fetch() fails with TypeError: fetch failed and the cause invalid onError method (UND_ERR_INVALID_ARG) when it is given a dispatcher built by an older npm undici: a plain Agent, a keep-alive agent, an SSRF guard, a client library's bundled agent or, most often, a proxy agent. The request never leaves the process. Every version, error string and result below comes from a lab run on 7 November 2026 or from the linked documentation and issues.
Quick fix
node -p "process.versions.undici"
npm ls undiciIf the first command prints 8.x and the second lists undici@5 or undici@6, that copy is the likely source. The fix that worked with every undici version on both Node lines in our tests is to import fetch from the package that builds the dispatcher: import { fetch, ProxyAgent } from 'undici' (Fix A below). If the dispatcher only exists to read HTTPS_PROXY, remove it and start Node with --use-env-proxy.
Tested on Node.js 26.13.0 (bundled undici 8.12.2) and 24.22.2 (bundled undici 7.30.1) with npm undici 5.29.0, 6.29.1, 7.30.2 and 8.12.4, on macOS 26.5.1 arm64, 7 November 2026. The scripts and all 166 recorded cases are in the lab folder.
The error: UND_ ERR_ INVALID_ ARG in err.cause
This is the complete output of the minimal reproduction shown further down, which passes an npm undici 6.29.1 Agent to the built-in fetch on Node.js 26.13.0. The lab path is shortened to /lab:
node:internal/modules/run_main:111
triggerUncaughtException(
^
[TypeError: fetch failed] {
[cause]: InvalidArgumentError: invalid onError method
at Agent.dispatch (/lab/u6/node_modules/undici/lib/dispatcher/dispatcher-base.js:198:15)
at node:internal/deps/undici/undici:15144:57
at new Promise (<anonymous>)
at dispatchWithProtocolPreference (node:internal/deps/undici/undici:15144:18)
at dispatch (node:internal/deps/undici/undici:15142:16)
at httpNetworkFetch (node:internal/deps/undici/undici:15040:73)
at httpNetworkOrCacheFetch (node:internal/deps/undici/undici:14912:39)
at httpFetch (node:internal/deps/undici/undici:14729:43)
at schemeFetch (node:internal/deps/undici/undici:14637:18)
at mainFetch (node:internal/deps/undici/undici:14481:30) {
code: 'UND_ERR_INVALID_ARG',
Symbol(undici.error.UND_ERR): true,
Symbol(undici.error.UND_ERR_INVALID_ARG): true
}
}
Node.js v26.13.0The first at line names the copy of undici that threw: here /lab/u6/node_modules/undici. With a ProxyAgent the trace gains one line, at ProxyAgent.dispatch, and the message is the same. The same script on Node.js 24.22.2 printed 200. Code that catches the error and logs only err.message prints just fetch failed; the reason is in err.cause.
Why Node 26 rejects older undici dispatchers
The built-in fetch is a copy of undici bundled into Node, and process.versions.undici tells you which one. The dispatcher option hands each request to the object you pass, so two undici versions have to agree on how they talk to each other. undici 8 renamed the handler callbacks a dispatcher calls, for example onError to onResponseError (migration guide). Node 26's fetch passes a handler with only the new callbacks. An undici 5 or 6 dispatcher checks for the old onError, finds none and throws before it opens a connection. undici 7 dispatchers accepted the new callbacks in our tests.
The minimal reproduction:
import { Agent } from 'undici';
const res = await fetch('https://example.com/', { dispatcher: new Agent() });
console.log(res.status);The same Agent, ProxyAgent and EnvHttpProxyAgent cases, passed per request to the built-in fetch:
| npm undici | Node.js 26.13.0 (undici 8.12.2) | Node.js 24.22.2 (undici 7.30.1) |
|---|---|---|
| 5.29.0 | invalid onError method | works |
| 6.29.1 | invalid onError method | works |
| 7.30.2 | works | works |
| 8.12.4 | works | invalid onRequestStart method |
undici 5.29.0 has no EnvHttpProxyAgent; every other cell held for all three agent types. Older releases behaved the same on Node 26: Agent and ProxyAgent from 5.0.0, 5.28.4, 6.0.0 and 6.21.0 failed with invalid onError method, and from the eight 7.x releases we tried, 7.0.0 to 7.28.0, they worked. The last row is the same mismatch in reverse: Node 24's fetch passes a handler without onRequestStart, and an undici 8 dispatcher rejects it.
Find what's bundling an old undici
Run both commands with the same node and in the same project as the failing code:
node -p "process.versions.undici"
npm ls undiciThe first printed 8.12.2 on Node 26.13.0 and 7.30.1 on Node 24.22.2. In a test project with its own undici 8 and cheerio@1.0.0, the second printed:
my-app@1.0.0 /lab/npm-ls-demo/my-app
├─┬ cheerio@1.0.0
│ └── undici@6.29.1
└── undici@8.12.4The top-level undici@8.12.4 is the project's own copy. cheerio@1.0.0 brings its own undici@6.29.1. A 5.x or 6.x entry is a candidate, not proof: it fails only if a dispatcher it builds is passed to the built-in fetch. The stack trace settles it, because its first at line shows the path of the copy that threw. A tool that compiles undici into a single file does not appear in npm ls at all; the Corepack trace in corepack#834 points into corepack.cjs.
Fixes A and B: pair fetch with its own undici
Fix A: use undici's own fetch
Import fetch from the same package as the dispatcher, so both halves always come from one version:
import { fetch, ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent(process.env.HTTPS_PROXY);
const res = await fetch('https://example.com/', { dispatcher });
console.log(res.status);In the lab this worked with undici 5.29.0, 6.29.1, 7.30.2 and 8.12.4 on both Node 26.13.0 and 24.22.2, with Agent and ProxyAgent, and with EnvHttpProxyAgent where the version has one; every proxied request reached the proxy. Use it in libraries, CLIs and anything that runs on more than one Node line. Keep FormData and other body classes from the same package as fetch (undici docs). The Serverless and career-ops fixes and the open Vercel CLI pull request linked below take this route.
Fix B: match the major to process.versions.undici
npm install undici@8With undici 8.12.4, the built-in fetch on Node 26.13.0 accepted Agent, ProxyAgent and EnvHttpProxyAgent. On Node 24.22.2 the same dispatchers failed with invalid onRequestStart method, so this fix ties your code to one Node line. undici 8 needs Node.js 22.19.0 or later (migration guide). undici 7.30.2 worked on both lines in the lab, and Redocly CLI and the Qdrant client moved to undici 7; that is a snapshot of two Node releases, not a promise about the next one.
Behind a proxy: ProxyAgent, EnvHttpProxyAgent, setGlobalDispatcher
ProxyAgent and EnvHttpProxyAgent from undici 5 or 6 fail exactly like a plain Agent. Adding NODE_USE_ENV_PROXY=1 does not help while your code still passes the old dispatcher: an undici 6.29.1 ProxyAgent failed with invalid onError method on Node 26.13.0 with the variable set.
setGlobalDispatcher() fails more quietly. On Node 26.13.0, a global ProxyAgent from undici 5.29.0, 6.29.1 or the 7.x releases we tried before 7.27.0 (7.0.0 to 7.26.0), and a global EnvHttpProxyAgent from 6.29.1, did not throw. The built-in fetch ignored them and connected directly. Against https://example.com, an undici 6.29.1 global ProxyAgent got a 200 while the proxy logged nothing, so the request left from our own IP; on Node 24.22.2 the same script went through the proxy. undici 7.27.0, 7.28.0, 7.30.2 and 8.12.4 worked globally. None of the older versions writes Symbol.for('undici.globalDispatcher.2'), where undici 8 keeps its global dispatcher; 7.27.0 and later do.
Fix C: let Node read the proxy variables
If the dispatcher only exists to send traffic through HTTPS_PROXY, remove it and let Node do it:
export HTTPS_PROXY=http://USER:PASS@proxy.example:8080
export NO_PROXY=localhost,127.0.0.1
node --use-env-proxy app.mjsNODE_USE_ENV_PROXY=1, the --use-env-proxy flag and NODE_OPTIONS=--use-env-proxy all worked on Node 26.13.0 and 24.22.2, for fetch and for https.get. The opt-in arrived for fetch first, in this Node commit; the Node docs list NODE_USE_ENV_PROXY from 24.0.0 and --use-env-proxy from 24.5.0, and both from 22.21.0. What the lab showed:
- Read at startup only. Setting
process.env.NODE_USE_ENV_PROXY = '1'inside the script did nothing. To switch on at runtime, callhttp.setGlobalProxyFromEnv()(added in 24.14.0 and 25.4.0); it proxied bothfetchandhttps.get. - HTTP_PROXY alone. With only
HTTP_PROXYset,fetchused it for anhttps://URL, buthttps.getconnected directly. SetHTTPS_PROXY. - NO_PROXY.
target.test,.test,*.test,*,target.test:8443and lowercaseno_proxyall bypassed the proxy;target.test:9999did not, because the port differs. WithNO_PROXYunset, requests tolocalhostand127.0.0.1went through the proxy;NO_PROXY=localhost,127.0.0.1sent them direct.
Node 26's fetch negotiated HTTP/2 with the lab's target, inside the CONNECT tunnel too, where Node 24 used HTTP/1.1. The proxy saw the same CONNECT line either way, and no request failed because of it.
Vercel CLI, Corepack, Redocly and other tools you don't control
Checked on 7 November 2026:
- Vercel CLI. vercel#17629 is open: with any proxy variable set, versions 59.18.0 and 59.23.1 fail on Node 26.8.2 with
invalid onError methodfrom a bundled undici 5.29.0 and work on Node 24.21.0. The fix, vercel#17634, is not merged. Until a release has it, run the CLI on Node 24. - Corepack. corepack#834 reports a bundled undici 6
ProxyAgentfailing on Node 26.0.0 and 26.1.0. Corepack 0.35.0 dropped undici and uses Node's proxy support, so setNODE_USE_ENV_PROXY=1; the reporter confirmed that combination works. The Node 26.13.0 tarball we installed has nocorepackbinary (24.22.2 has one), so check which version you installed withcorepack --version. - Redocly CLI. redocly-cli#3232 covers
login,pushand the other Reunite commands. 2.62.2 fixed them on Node 26 and moved to undici 7.30.0. - Serverless Framework. 4.44.0 fixed the npm package's proxied download on Node 26 (#13918), and 4.45.0 fixed its API requests through a proxy on Node 26.
- Client libraries, no proxy involved. SocratiCode#154:
@qdrant/js-client-restbefore 1.19 hands an undici 6Agentto the built-infetch, and 1.19.0 moved to undici 7. almyty#702: an SSRF-pinningAgentfrom undici 6.27.0 failed on Node 26.9.0 and its custom DNS lookup never ran; the fix raised undici to ^8.11.0. career-ops#4808: proxy egress with an undici 6EnvHttpProxyAgentfailed on Node 26; v1.36.0 pairs it with undici's ownfetch, the same approach as the earlier #4787.
For any other tool: upgrade it, run it on Node 24 meanwhile, and report the error with its cause and the first at line.
Check it really goes through the proxy
A response with status 200 does not prove the proxy carried it: an ignored global dispatcher or a NO_PROXY match sends the request direct, and a reachable site still answers 200. Look at the proxy's own log first; in the lab every proxied request left one CONNECT line with the host and port, and the bypassed ones left none. Without access to the proxy log, ask an IP-echo service:
const res = await fetch('https://api.ipify.org?format=json');
console.log(await res.json());Run it once through your proxy setup, for example with HTTPS_PROXY set and node --use-env-proxy, and once without it. If both print the same IP, the request went around the proxy.
FAQ
Does undici 7 cause "invalid onError method" on Node.js 26?
Not in our tests. Agent and ProxyAgent from every 7.x release we tried, 7.0.0 to 7.30.2, worked when passed per request, and so did EnvHttpProxyAgent from 7.30.2. setGlobalDispatcher() from 7.x before 7.27.0 was ignored without an error.
Why does it work on Node 24 and fail on Node 26?
Node 24.22.2 bundles undici 7.30.1, which accepted undici 5 and 6 dispatchers. Node 26.13.0 bundles undici 8.12.2, which sends only the new callbacks.
Does NODE_USE_ENV_PROXY=1 fix it?
Only after you remove the old dispatcher. With an undici 6 ProxyAgent still passed to fetch, Node 26 failed the same way with the variable set.
I see "invalid onRequestStart method" instead. Is it the same problem?
Yes, in reverse: an undici 8 dispatcher passed to the fetch of an older Node line; we saw it on Node 24.22.2. Fix A works there too.
Related guides
- Fix Node.js TypeError: fetch failed behind a proxy reads every cause in the chain, not only this one.
- Use a proxy with Node.js fetch sets up a
ProxyAgentwith authentication and a deadline. - Node.js fetch ignores HTTPS_PROXY covers the timeout you get without the opt-in.
- Proxy environment variables compares how curl, Requests and Node read them.
- npm proxy settings gets
npm install undicithrough a proxy.
Test the fix through a real carrier IP. An ipvolt mobile proxy is a dedicated device on a carrier network, from $9 a day. Put its URL in HTTPS_PROXY and run the IP check above with --use-env-proxy.
Sources & further reading
Technical references used for this guide. Check the documentation for your installed version and your provider’s supported configuration.
- undici: Migrating from undici 7 to 8 (handler callbacks, global dispatcher)
- undici: Undici module vs. Node.js built-in fetch
- Node.js CLI: --use-env-proxy
- Node.js CLI: NODE_USE_ENV_PROXY=1
- Node.js HTTP: Built-in proxy support and NO_PROXY format
- Node.js HTTP: http.setGlobalProxyFromEnv()
- Node.js commit ada34bd: proxy support for fetch under NODE_USE_ENV_PROXY
- vercel/vercel#17629: CLI fails on Node.js 26 with a proxy variable set
- vercel/vercel#17634: route the CLI through its bundled undici fetch
- nodejs/corepack#834: bundled undici 6 ProxyAgent on Node.js 26
- Corepack 0.35.0 release notes
- Redocly/redocly-cli#3232: Reunite commands fail on Node.js 26
- Redocly CLI 2.62.2 release notes
- serverless/serverless#13918: proxied binary downloads on Node.js 26
- Serverless Framework 4.44.0 release notes
- Serverless Framework 4.45.0 release notes
- giancarloerra/SocratiCode#154: Qdrant client before 1.19 on Node.js 26
- almyty-inc/almyty#702: SSRF-pinning agent and undici 8
- career-ops-hq/career-ops#4808: proxy egress fails on Node.js 26
- career-ops-hq/career-ops#4787: drive the proxy agent with undici's own fetch