Node.js fetch 报 ETIMEDOUT,检查地址族的单次连接等待时间
fetch 的 AggregateError 同时含 ETIMEDOUT 与 ENETUNREACH,先比较重定向与网络路径,再核对单次地址连接等待。
同一台机器上 curl 能访问,Node.js fetch() 却报 TypeError: fetch failed。展开 cause 后,可能看到 AggregateError:一部分 IPv4 地址报 ETIMEDOUT,一部分 IPv6 地址报 ENETUNREACH。这组信息指向连接尝试阶段,外层的 fetch failed 本身并不能说明具体原因。
本文处理的是自动选择地址族时,一次连接尝试的等待时间可能不够的分支。DNS 返回多个地址、到目标网络的路由不同,以及 HTTP 重定向,都会影响对照结果。它与收到响应后等待正文超时、请求整体被取消,以及其他 Undici 超时错误,需要分别判断。
curl 成功时,先确认比较的是同一条请求链
curl 默认不跟随重定向,fetch 默认会跟随。curl 只取得初始地址的 301,fetch 尝试访问后续地址却失败,两者并没有完成相同的请求。对可信、无认证的只读地址,可以核对两边最终 URL、跳转链、方法、代理和实际连接地址,再比较结果。
不要为方便对照把认证头或完整 Cookie 转发给新主机。若原请求经过环境代理、框架自定义 dispatcher 或网关,这些条件也要记下来;本机直连成功不能代替运行容器内的路径。
出现 ENETUNREACH 时,检查相应地址族是否真的有可用路由。出现多个 IPv4 ETIMEDOUT 时,也不能只因日志包含 IPv6 就断言“禁用 IPv6 即可”。连接超时仍可能来自防火墙、目标端限流或线路问题。
单次尝试超时与整个 HTTP 请求超时不同
Node 的 autoSelectFamily 在适用条件下依次尝试解析得到的 IPv6、IPv4 地址,直到连接成功;并不是把所有地址同时连接。前面的尝试会受 autoSelectFamilyAttemptTimeout 限制,再转到下一个地址。最后一次尝试不按同一切换计时规则处理。
这个选项不是响应头超时、正文超时,也不是整条请求的时限。固定了非零 family 或设置 localAddress 时,自动地址族选择还可能不适用。只有确认实际连接走的是这条路径,调整该值才有意义。
在 Node.js 20.11.1 的规则中,初始默认值是 250 毫秒。其他版本和启动参数可能改变默认值,排查时先读取目标进程采用的值。以下为 CommonJS 检查片段:
const net = require('node:net');
console.log(net.getDefaultAutoSelectFamilyAttemptTimeout());
应在与失败程序相同的运行版本和启动条件下检查。另起一个没有原启动参数的 Node 进程,读取到的值不一定代表服务的实际值;调用方显式传入的连接选项也可能覆盖默认值。
在受控启动入口试调,保留整体截止时间
若日志和网络对照确实支持“有效地址的连接尚未完成就切换”这一分支,可以在受控环境中试调。例如把启动时的默认尝试时间改为 500 毫秒:
const net = require('node:net');
const previousAttemptTimeout = net.getDefaultAutoSelectFamilyAttemptTimeout();
net.setDefaultAutoSelectFamilyAttemptTimeout(500);
// 随后启动本应用原有的初始化和请求流程。
这是示例值,不是所有线路的推荐参数。它修改当前 Node 进程的默认值,可能影响其他没有显式指定该选项的连接;应在发起请求前配置,并保留应用原有的整体截止时间。等待一个无效地址更久,也可能增加失败延迟。
支持该参数的版本还可以使用启动选项 --network-family-autoselection-attempt-timeout,但它从 Node.js 20.13.0、22.1.0 才加入,不能直接用于上述 20.11.1 环境。使用外部 Undici 的 Agent 或 Client 时,应另外核对已安装版本的连接配置,不要把更换整个全局 dispatcher 当成无影响的检查。
试调后用原来的只读请求检查实际连接结果、失败地址和耗时;偶然成功一次不足以证明长期恢复。仍然失败时,回到 DNS、路由、代理、目标地址及跳转链继续缩小范围。撤回试调可恢复记录的默认值并按原配置重新启动;这不会倒转已经发起的连接,也不能用来取消正在进行的业务请求。


