← 中文资源

简体中文版本

Node.js fetch — 慢上游导致 UND_ERR_HEADERS_TIMEOUT

解释 Node 全局 fetch 报 UND_ERR_HEADERS_TIMEOUT 的机制,测量响应头卡在哪一段,并按端点配置 undici 超时而非全局一刀切。

阅读英文原文

症状

错误藏在通用的 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),
});