fetch failed: invalid onError method on Node 26 (undici fix)

Node.js 26 fetch fails with invalid onError method (UND_ERR_INVALID_ARG) when given an undici 5 or 6 Agent or ProxyAgent. Fix: use undici's own fetch.

On this page

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

sh
node -p "process.versions.undici"
npm ls undici

If 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:

code
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.0

The 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:

js
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 undiciNode.js 26.13.0 (undici 8.12.2)Node.js 24.22.2 (undici 7.30.1)
5.29.0invalid onError methodworks
6.29.1invalid onError methodworks
7.30.2worksworks
8.12.4worksinvalid 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:

sh
node -p "process.versions.undici"
npm ls undici

The 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:

code
my-app@1.0.0 /lab/npm-ls-demo/my-app
├─┬ cheerio@1.0.0
│ └── undici@6.29.1
└── undici@8.12.4

The 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:

js
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

sh
npm install undici@8

With 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:

sh
export HTTPS_PROXY=http://USER:PASS@proxy.example:8080
export NO_PROXY=localhost,127.0.0.1
node --use-env-proxy app.mjs

NODE_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, call http.setGlobalProxyFromEnv() (added in 24.14.0 and 25.4.0); it proxied both fetch and https.get.
  • HTTP_PROXY alone. With only HTTP_PROXY set, fetch used it for an https:// URL, but https.get connected directly. Set HTTPS_PROXY.
  • NO_PROXY. target.test, .test, *.test, *, target.test:8443 and lowercase no_proxy all bypassed the proxy; target.test:9999 did not, because the port differs. With NO_PROXY unset, requests to localhost and 127.0.0.1 went through the proxy; NO_PROXY=localhost,127.0.0.1 sent 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 method from 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 ProxyAgent failing on Node 26.0.0 and 26.1.0. Corepack 0.35.0 dropped undici and uses Node's proxy support, so set NODE_USE_ENV_PROXY=1; the reporter confirmed that combination works. The Node 26.13.0 tarball we installed has no corepack binary (24.22.2 has one), so check which version you installed with corepack --version.
  • Redocly CLI. redocly-cli#3232 covers login, push and 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-rest before 1.19 hands an undici 6 Agent to the built-in fetch, and 1.19.0 moved to undici 7. almyty#702: an SSRF-pinning Agent from 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 6 EnvHttpProxyAgent failed on Node 26; v1.36.0 pairs it with undici's own fetch, 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:

js
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.

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.