在 Node.js 26 上,如果传给 fetch() 的 dispatcher 是由旧版 npm undici 创建的,它就会以 TypeError: fetch failed 失败,原因是 invalid onError method(UND_ERR_INVALID_ARG)。这个 dispatcher 可能是普通的 Agent、keep-alive agent、SSRF 防护、客户端库自带的 agent,最常见的则是代理 agent。请求根本没有离开进程。下文的每个版本号、错误字符串和结果,都来自 2026 年 11 月 7 日的实验运行,或来自所链接的文档和 issue。
快速修复
node -p "process.versions.undici"
npm ls undici如果第一条命令输出 8.x,而第二条列出了 undici@5 或 undici@6,那么这份副本很可能就是来源。在我们的测试中,对所有 undici 版本、在两条 Node 版本线上都有效的修复是:从创建 dispatcher 的同一个包里导入 fetch,即 import { fetch, ProxyAgent } from 'undici'(见下文修复 A)。如果这个 dispatcher 只是为了读取 HTTPS_PROXY,就删掉它,改用 --use-env-proxy 启动 Node。
测试环境:Node.js 26.13.0(内置 undici 8.12.2)和 24.22.2(内置 undici 7.30.1),npm undici 5.29.0、6.29.1、7.30.2 和 8.12.4,macOS 26.5.1 arm64,2026 年 11 月 7 日。脚本和全部 166 个记录的用例都在实验文件夹中。
错误信息:err.cause 中的 UND_ ERR_ INVALID_ ARG
下面是后文最小复现脚本的完整输出:它把 npm undici 6.29.1 的 Agent 传给 Node.js 26.13.0 的内置 fetch。实验路径已缩写为 /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.0第一行 at 指出了抛出错误的 undici 副本:这里是 /lab/u6/node_modules/undici。换成 ProxyAgent 时,堆栈会多出一行 at ProxyAgent.dispatch,错误信息不变。同一个脚本在 Node.js 24.22.2 上输出 200。如果代码捕获错误后只记录 err.message,你只会看到 fetch failed;真正的原因在 err.cause 里。
为什么 Node 26 会拒绝旧版 undici 的 dispatcher
内置 fetch 是打包进 Node 的一份 undici,process.versions.undici 会告诉你是哪个版本。dispatcher 选项会把每个请求交给你传入的对象,所以两个 undici 版本必须在彼此通信的方式上达成一致。undici 8 重命名了 dispatcher 调用的处理器回调,例如 onError 改成了 onResponseError(迁移指南)。Node 26 的 fetch 传入的处理器只带新回调。undici 5 或 6 的 dispatcher 会检查旧的 onError,找不到就在建立连接之前抛出错误。在我们的测试中,undici 7 的 dispatcher 接受了新回调。
最小复现:
import { Agent } from 'undici';
const res = await fetch('https://example.com/', { dispatcher: new Agent() });
console.log(res.status);把 Agent、ProxyAgent 和 EnvHttpProxyAgent 按请求传给内置 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 | 正常 |
| 6.29.1 | invalid onError method | 正常 |
| 7.30.2 | 正常 | 正常 |
| 8.12.4 | 正常 | invalid onRequestStart method |
undici 5.29.0 没有 EnvHttpProxyAgent;其余每个单元格对三种 agent 都成立。较旧的版本在 Node 26 上表现相同:5.0.0、5.28.4、6.0.0 和 6.21.0 的 Agent 与 ProxyAgent 都以 invalid onError method 失败,而我们试过的八个 7.x 版本(7.0.0 到 7.28.0)都正常。最后一行是方向相反的同一种错配:Node 24 的 fetch 传入的处理器没有 onRequestStart,undici 8 的 dispatcher 会拒绝它。
找出是谁带来了旧版 undici
用同一个 node、在出错代码所在的同一个项目里运行这两条命令:
node -p "process.versions.undici"
npm ls undici第一条在 Node 26.13.0 上输出 8.12.2,在 Node 24.22.2 上输出 7.30.1。在一个自带 undici 8 并安装了 cheerio@1.0.0 的测试项目里,第二条输出:
my-app@1.0.0 /lab/npm-ls-demo/my-app
├─┬ cheerio@1.0.0
│ └── undici@6.29.1
└── undici@8.12.4顶层的 undici@8.12.4 是项目自己的副本,cheerio@1.0.0 则带来了它自己的 undici@6.29.1。5.x 或 6.x 条目只是嫌疑对象,不是证据:只有当它创建的 dispatcher 被传给内置 fetch 时才会出错。堆栈可以给出定论,因为它的第一行 at 显示了抛出错误的那份副本的路径。把 undici 编译进单个文件的工具根本不会出现在 npm ls 中;corepack#834 里的 Corepack 堆栈指向的是 corepack.cjs 内部。
修复 A 和 B:让 fetch 与 dispatcher 来自同一个 undici
修复 A:使用 undici 自己的 fetch
从 dispatcher 所在的同一个包导入 fetch,让两者始终来自同一个版本:
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);在实验中,它在 Node 26.13.0 和 24.22.2 上对 undici 5.29.0、6.29.1、7.30.2 和 8.12.4 都有效,适用于 Agent 和 ProxyAgent,以及有 EnvHttpProxyAgent 的版本中的 EnvHttpProxyAgent;每个走代理的请求都到达了代理。库、CLI 以及需要在多条 Node 版本线上运行的代码都应该这样做。FormData 等请求体类也要和 fetch 来自同一个包(undici 文档)。下文链接的 Serverless 和 career-ops 修复,以及 Vercel CLI 尚未合并的 pull request,走的都是这条路。
修复 B:主版本与 process.versions.undici 一致
npm install undici@8使用 undici 8.12.4 时,Node 26.13.0 的内置 fetch 接受了 Agent、ProxyAgent 和 EnvHttpProxyAgent。在 Node 24.22.2 上,同样的 dispatcher 以 invalid onRequestStart method 失败,所以这种修复会把你的代码绑定在一条 Node 版本线上。undici 8 需要 Node.js 22.19.0 或更高版本(迁移指南)。实验中 undici 7.30.2 在两条版本线上都正常,Redocly CLI 和 Qdrant 客户端也改用了 undici 7;但这只是两个 Node 版本的快照,并不保证下一个版本也如此。
使用代理时:ProxyAgent、EnvHttpProxyAgent 与 setGlobalDispatcher
undici 5 或 6 的 ProxyAgent 和 EnvHttpProxyAgent 与普通 Agent 的失败方式完全相同。只要代码仍在传入旧的 dispatcher,加上 NODE_USE_ENV_PROXY=1 也没用:设置了该变量后,undici 6.29.1 的 ProxyAgent 在 Node 26.13.0 上仍以 invalid onError method 失败。
setGlobalDispatcher() 的失败更隐蔽。在 Node 26.13.0 上,来自 undici 5.29.0、6.29.1 或我们试过的 7.27.0 之前的 7.x 版本(7.0.0 到 7.26.0)的全局 ProxyAgent,以及来自 6.29.1 的全局 EnvHttpProxyAgent,都没有抛出错误。内置 fetch 直接忽略了它们并直连。请求 https://example.com 时,undici 6.29.1 的全局 ProxyAgent 拿到了 200,而代理日志里什么都没有,也就是说请求是从我们自己的 IP 发出的;同一个脚本在 Node 24.22.2 上则走了代理。undici 7.27.0、7.28.0、7.30.2 和 8.12.4 的全局设置都有效。旧版本都不会写入 Symbol.for('undici.globalDispatcher.2'),而 undici 8 把全局 dispatcher 保存在那里;7.27.0 及以后的版本会写入。
修复 C:让 Node 自己读取代理变量
如果 dispatcher 只是为了让流量走 HTTPS_PROXY,就删掉它,交给 Node 处理:
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、--use-env-proxy 参数和 NODE_OPTIONS=--use-env-proxy 在 Node 26.13.0 和 24.22.2 上都有效,fetch 和 https.get 都适用。这项开关最先是为 fetch 加入的,见这个 Node 提交;Node 文档记载 NODE_USE_ENV_PROXY 从 24.0.0 起、--use-env-proxy 从 24.5.0 起可用,两者在 22.21.0 中也都可用。实验显示:
- 只在启动时读取。 在脚本里设置
process.env.NODE_USE_ENV_PROXY = '1'没有任何效果。要在运行时启用,请调用http.setGlobalProxyFromEnv()(24.14.0 和 25.4.0 中加入);它让fetch和https.get都走了代理。 - 只设置 HTTP_PROXY。 只设置
HTTP_PROXY时,fetch会把它用于https://URL,但https.get会直连。请设置HTTPS_PROXY。 - NO_PROXY。
target.test、.test、*.test、*、target.test:8443以及小写的no_proxy都绕过了代理;target.test:9999没有绕过,因为端口不同。未设置NO_PROXY时,发往localhost和127.0.0.1的请求会走代理;设置NO_PROXY=localhost,127.0.0.1后它们改为直连。
Node 26 的 fetch 与实验目标协商出了 HTTP/2,在 CONNECT 隧道内也是如此,而 Node 24 用的是 HTTP/1.1。两种情况下代理看到的都是同样的 CONNECT 行,也没有请求因此失败。
Vercel CLI、Corepack、Redocly 等你无法控制的工具
核对日期:2026 年 11 月 7 日。
- Vercel CLI。 vercel#17629 仍未关闭:只要设置了任意代理变量,59.18.0 和 59.23.1 版本在 Node 26.8.2 上就会因内置的 undici 5.29.0 以
invalid onError method失败,在 Node 24.21.0 上则正常。修复 vercel#17634 尚未合并。在有版本包含该修复之前,请用 Node 24 运行这个 CLI。 - Corepack。 corepack#834 报告了内置的 undici 6
ProxyAgent在 Node 26.0.0 和 26.1.0 上失败。Corepack 0.35.0 去掉了 undici,改用 Node 的代理支持,所以请设置NODE_USE_ENV_PROXY=1;issue 作者确认了这个组合可用。我们安装的 Node 26.13.0 压缩包里没有corepack可执行文件(24.22.2 里有),所以请用corepack --version检查你安装的版本。 - Redocly CLI。 redocly-cli#3232 涉及
login、push以及其他 Reunite 命令。2.62.2 修复了它们在 Node 26 上的问题,并改用 undici 7.30.0。 - Serverless Framework。 4.44.0 修复了 npm 包在 Node 26 上通过代理下载的问题(#13918),4.45.0 修复了它在 Node 26 上通过代理发出的 API 请求。
- 客户端库,不涉及代理。 SocratiCode#154:1.19 之前的
@qdrant/js-client-rest会把 undici 6 的Agent传给内置fetch,1.19.0 改用了 undici 7。almyty#702:一个来自 undici 6.27.0、带 SSRF 绑定的Agent在 Node 26.9.0 上失败,它自定义的 DNS 查询从未执行;修复把 undici 升到了 ^8.11.0。career-ops#4808:使用 undici 6EnvHttpProxyAgent的代理出口在 Node 26 上失败;v1.36.0 让它搭配 undici 自己的fetch,与更早的 #4787 思路相同。
对于其他工具:先升级;在此之前用 Node 24 运行,并在报告问题时附上错误的 cause 和第一行 at。
确认请求确实经过代理
状态码 200 并不能证明响应是代理送来的:被忽略的全局 dispatcher 或匹配上的 NO_PROXY 都会让请求直连,而可访问的网站照样返回 200。先看代理自己的日志;在实验中,每个走代理的请求都会留下一行带主机和端口的 CONNECT,绕过代理的请求则一行都没有。如果看不到代理日志,就问一个返回你 IP 的服务:
const res = await fetch('https://api.ipify.org?format=json');
console.log(await res.json());用你的代理配置运行一次,例如设置 HTTPS_PROXY 并使用 node --use-env-proxy,再不带代理运行一次。如果两次输出的 IP 相同,说明请求绕过了代理。
常见问题
undici 7 会在 Node.js 26 上导致 “invalid onError method” 吗?
在我们的测试中不会。我们试过的所有 7.x 版本(7.0.0 到 7.30.2)的 Agent 和 ProxyAgent 按请求传入时都正常,7.30.2 的 EnvHttpProxyAgent 也一样。7.27.0 之前的 7.x 中的 setGlobalDispatcher() 会被悄无声息地忽略。
为什么在 Node 24 上正常,在 Node 26 上失败?
Node 24.22.2 内置 undici 7.30.1,它接受 undici 5 和 6 的 dispatcher。Node 26.13.0 内置 undici 8.12.2,它只传入新回调。
NODE_USE_ENV_PROXY=1 能解决吗?
只有在删掉旧 dispatcher 之后才行。只要 undici 6 的 ProxyAgent 仍被传给 fetch,设置了该变量的 Node 26 照样失败。
我看到的是 “invalid onRequestStart method”,是同一个问题吗?
是的,方向相反:undici 8 的 dispatcher 被传给了较旧 Node 版本线的 fetch;我们在 Node 24.22.2 上看到了它。修复 A 在这种情况下同样有效。
相关指南
- 修复代理环境下 Node.js 的 TypeError: fetch failed逐层解读错误链中的每种原因,而不只是这一种。
- 在 Node.js fetch 中使用代理介绍如何配置带认证和超时的
ProxyAgent。 - Node.js fetch 忽略 HTTPS_PROXY解释不显式启用时出现的超时。
- 代理环境变量比较 curl、Requests 和 Node 如何读取这些变量。
- npm 代理设置(英文)让
npm install undici能通过代理完成。
用真实的运营商 IP 测试这个修复。 ipvolt 的移动代理是运营商网络中的专用设备,每天 $9 起。把它的 URL 填入 HTTPS_PROXY,然后用 --use-env-proxy 运行上面的 IP 检查。
参考来源与延伸阅读
本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。
- 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