症状
错误藏在通用的 fetch failed 之下,先解包 cause 才能看到 UND_ERR_HEADERS_TIMEOUT。时间特征是指纹:精确的 300 秒——这正是 undici 的 headersTimeout 默认值,它限制的是“发出请求到收到首个响应头”,不是总时长。用 curl -w 的 time_starttransfer 字段测量服务端事实,当该值超过 300 秒,故障就是客户端在执行一条端点合法越过的策略;curl 成功是因为它根本不设这条限制。
诊断思路
<arg_value>先给失败调用分相计时(DNS、连接、首字节、总时长),ttfb 超过 300 秒即确认服务器在响应头之前就要花这么久;再确认 Node 全局 fetch 走 undici dispatcher 及其默认值(headersTimeout 与 bodyTimeout 各 300 秒);最后用故意延迟 305 秒的端点确定性复现 UND_ERR_HEADERS_TIMEOUT,证明边界是“响应头到达”而非总时长。代理链在应用服务器前的排队延迟会隐形消耗预算,是常见放大因素。
根因
Node 全局 fetch 经由 undici dispatcher 发请求,其 headersTimeout 约束从发送请求到首个响应头的等待时间。批处理报表、冷缓存、长查询这类合法需要超过该窗口的端点会越过边界,请求在任何状态码或响应体存在之前就已失败。
修复与验证
为慢端点创建专属 dispatcher,单独调高 headersTimeout 与 bodyTimeout,不要改全局默认值——全局调高会让所有端点都能把连接占住几分钟,高负载下耗尽连接池。同时用 AbortSignal.timeout 显式声明每请求的外层预算,让限制保持可读。若端点可以改造,更强的修复是架构性的:立即返回 202 附带状态查询 URL,轮询结果,彻底移除长占连接。验证时用 scoped dispatcher 重跑慢调用确认跨过旧的 300 秒墙仍能完成;再验证作用域:带短 AbortSignal 的快端点必须仍在原时限失败,证明全局默认未被触碰;批处理运行期间观察连接指标,202 模式下连接应短暂且池占用平稳。
代码示例
TypeError: fetch failed
cause: Error: Headers Timeout Error
code: 'UND_ERR_HEADERS_TIMEOUT'
import { Agent, fetch as undiciFetch } from 'undici';
const slowAgent = new Agent({
headersTimeout: 900_000, // 15 minutes: send -> first header
bodyTimeout: 900_000, // header -> last body byte
});
const res = await undiciFetch(url, { dispatcher: slowAgent });
const res = await undiciFetch(url, {
dispatcher: slowAgent,
signal: AbortSignal.timeout(15 * 60_000),
});