集成阅读约 4 分钟

在 Node.js fetch 中使用代理

让 Node.js 原生 fetch 经由 Undici ProxyAgent 走代理,并配置显式的代理认证、中止期限和响应体清理。

本页内容

起点

为单次 fetch 调用使用一个 dispatcher,使代理配置显式化,进程中的其他网络客户端则保留各自的配置。

选择兼容的运行时与 dispatcher

这个服务端示例面向 Node.js 24 和 Undici 7。agent 必须来自运行时内置 fetch 所接受的 undici 主版本:在 Node 26 上,undici 5 和 6 的 agent 会以 invalid onError method 失败;在 Node 22 或 24 上,undici 8 的 agent 会以 invalid onRequestStart method 失败(两者的修复方法)。在一个单独的示例项目中运行 npm install undici@7。Node 内置的 fetch 接受与 Undici 兼容的 dispatcher。这与浏览器中的 fetch 不同,页面 JavaScript 无法任意选择系统代理。

在 PROXY_URL 中使用提供商文档给出的 HTTP(S) 网关,例如刻意无法工作的 https://proxy.example.invalid:8443。通过部署环境的密钥管理器注入 PROXY_USERNAME 和 PROXY_PASSWORD。不要把网关凭据放进前端代码。

把代理认证放在 dispatcher 上

保存为 proxy-check.mjs 并运行 node proxy-check.mjs。Basic 令牌属于代理 agent,而不是目标站点的 Authorization 请求头。该请求有 30 秒的中止期限,并且不会自动跟随重定向。

proxy-check.mjs · Node.js 24 + Undici 7

javascript
import { ProxyAgent } from 'undici';

function required(name) {
  const value = process.env[name];
  if (!value) throw new Error('Missing environment variable: ' + name);
  return value;
}

const gateway = new URL(required('PROXY_URL'));
if (!['http:', 'https:'].includes(gateway.protocol) || gateway.username || gateway.password) {
  throw new Error('Use a credential-free HTTP(S) proxy URL');
}
const credentials = required('PROXY_USERNAME') + ':' + required('PROXY_PASSWORD');
const dispatcher = new ProxyAgent({
  uri: gateway.href,
  token: 'Basic ' + Buffer.from(credentials).toString('base64'),
});

try {
  const response = await fetch('https://example.com/', {
    dispatcher,
    signal: AbortSignal.timeout(30_000),
    redirect: 'manual',
  });
  await response.body?.cancel();
  if (!response.ok) throw new Error('Destination HTTP ' + response.status);
  console.log({ status: response.status });
} finally {
  await dispatcher.close();
}

从诊断脚本走向工作进程

这个脚本刻意取消了响应体,因为诊断只需要状态码。使用结果的工作进程应在其预算内消费响应体。在继续之前务必消费或取消它,并在工作进程关闭时关闭 dispatcher。

对于重复的任务,复用一个并发受限的 dispatcher,而不是为每个请求创建一个。让每个任务的期限彼此独立。除非有意改变每个受影响客户端的路由,否则不要安装进程级的 dispatcher。

当请求确实失败时,先打印完整的 err.cause 链再做任何改动:修复 Node.js 代理后的 TypeError: fetch failed 逐一解读每个原因字符串,从被忽略的 HTTPS_PROXY、undici 版本不匹配,到被错误隐藏的 CONNECT 状态码。

  • HTTP 错误是一个响应;fetch 不会仅仅因为状态码是 4xx 或 5xx 而 reject。
  • 被拒绝的 CONNECT(例如 407 或 403)会在任何 Response 存在之前就以 TypeError: fetch failed reject;真正的原因在 err.cause 中,位于下一层或再下一层。
  • 记录脱敏后的错误类别;不要记录 Basic 令牌或包含凭据的完整 URL。

上线之前

  • 使用文档中给出的 Node 和 Undici 主版本。
  • 显式传入 dispatcher。
  • 消费或取消响应体,并关闭 agent。

参考来源与延伸阅读

本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。