起点
为单次 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
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 failedreject;真正的原因在err.cause中,位于下一层或再下一层。 - 记录脱敏后的错误类别;不要记录 Basic 令牌或包含凭据的完整 URL。
上线之前
- 使用文档中给出的 Node 和 Undici 主版本。
- 显式传入 dispatcher。
- 消费或取消响应体,并关闭 agent。
参考来源与延伸阅读
本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。