Node.js 代理设置:Axios、Fetch、Undici 和轮换

axios 的代理选项在 HTTPS 目标上是个陷阱,而原生 fetch 完全忽略你的代理环境变量。以下是适用于 axios、fetch 和 undici 的设置——代理、认证、SOCKS5、轮换和流式传输。

通过代理路由 Node.js 请求看似简单,却可能导致一下午的调试。原因在于大多数项目使用的三个 HTTP 客户端——axios、Node 18 中作为全局提供的原生 fetch,以及底层的 undici 库——对代理的处理方式各不相同,且都不如预期。Axios 有一个内置的 proxy 配置,文档中有说明,仅适用于 Node,但在 HTTP 代理后的 HTTPS 目标上悄然失效:这是一个自 2020 年以来在 axios GitHub 追踪器上开放的 bug,并引发了一个高票的 Stack Overflow 讨论。与此同时,原生 fetch 忽略 HTTP_PROXY 环境变量,且没有任何代理选项。本指南为每个客户端提供了实际可行的设置,以及认证、SOCKS5、轮换和流式传输。

Axios 代理配置与代理代理的对比

Axios 附带一个 proxy 对象——{ host, port, auth },对于普通 HTTP 目标工作良好。陷阱在于 HTTPS:当目标是 https:// 且代理使用 HTTP 时,axios 无法打开 CONNECT 隧道,你的请求要么挂起,要么返回真实 IP。社区达成的解决方案是完全绕过内置配置,给 axios 提供一个代理代理,然后设置 proxy: false 以避免两种机制冲突:

import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";

// Note the destructured import — a default import is the classic gotcha.
const agent = new HttpsProxyAgent("http://USER:PASS@residentialpremium.quantumproxies.io:8080");

const client = axios.create({
  httpAgent: agent,   // for http:// targets
  httpsAgent: agent,  // for https:// targets
  proxy: false,       // disable axios' own broken proxy handling
  timeout: 15000,
});

const r = await client.get("https://httpbin.org/ip");
console.log(r.data); // { origin: "<proxy exit IP>" }

两个细节可以节省数小时。首先,使用解构的 { HttpsProxyAgent } 导入——最近的版本将其作为命名符号导出,而默认导入会给你一个在构造时抛出异常的对象。其次,即使代理传输 HTTPS 流量,代理 URL 方案仍保持 http://:方案描述如何到达代理,而到目标的 TLS 在隧道内运行。同样的故障类别也出现在 Python 中;如果你也使用 requests,模式与我们在 ProxyError 和 SSLError 修复指南 中的相似。

认证和环境变量

认证代理使用 HTTP 基本凭据。使用代理时,将它们嵌入 URL 中,如 http://user:pass@host:port;如果密码包含 @、: 或 /,先用 encodeURIComponent() 进行 URL 编码,否则 URL 会在错误位置拆分。407 Proxy Authentication Required 表示凭据被拒绝或你的源 IP 未被列入白名单。Axios 还会从环境中读取 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY——对于不想修改代码的第三方库代理非常方便——通过设置 proxy: false 来禁用它。重要警告:原生 fetch 不读取这些变量,因此依赖环境变量代理的抓取器在从 axios 切换到 fetch 时会悄然直接连接。

同一请求的三段代码:axios 在 httpAgent 和 httpsAgent 上使用 HttpsProxyAgent 并设置 proxy: false,原生 fetch 在每次调用时传入 undici dispatcher,而 undici 的 setGlobalDispatcher 配合 ProxyAgent 可为整个进程设置代理。把它们混用是“proxy not working”的头号原因。
相同的请求,三种管道系统。Axios 需要代理,原生 fetch 需要 undici 调度器,混淆它们是“代理无法工作”的首要原因。

使用 undici 代理原生 fetch

Node 的全局 fetch 基于 undici 构建,代理也在 undici 中。你创建一个 ProxyAgent 并将其作为非标准 dispatcher 选项传递——这是代理 fetch 的现代、轻依赖方式,开箱即用地正确处理 HTTPS CONNECT:

import { ProxyAgent } from "undici";

const dispatcher = new ProxyAgent({
  uri: "http://residentialpremium.quantumproxies.io:8080",
  token: "Basic " + Buffer.from("USER:PASS").toString("base64"),
});

const res = await fetch("https://httpbin.org/ip", { dispatcher });
console.log(await res.json());

如果你直接使用 undici 而不是全局 fetch,相同的 ProxyAgent 可以插入到 request() 或 setGlobalDispatcher() 中,一次性代理进程中的每个 fetch。这种单次调用的全局切换是通过代理路由整个代码库的最简洁方式,无需在每个函数中传递调度器。

Node.js 中的 SOCKS5 代理

axios 和 undici 都不原生支持 SOCKS——将 socks5:// 字符串传递给 axios 的 proxy 配置会导致 protocol mismatch 断言。安装 socks-proxy-agent 并以与 HTTPS 代理相同的方式使用它,连接到 httpAgent 和 httpsAgent。优先使用 socks5h:// 而不是 socks5://:结尾的 h 在代理端解析 DNS,防止 DNS 泄漏并从出口位置解析地理限制的主机名。每个 SOCKS5 代理 计划通过 HTTP 和 SOCKS5 暴露相同的网关,因此这是方案的切换,而不是新的购买:

import axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";

const agent = new SocksProxyAgent("socks5h://USER:PASS@residentialpremium.quantumproxies.io:1080");

const client = axios.create({ httpAgent: agent, httpsAgent: agent });
const r = await client.get("https://httpbin.org/ip");
console.log(r.data);

没有代理列表的轮换

旧的方案——一个 IP 数组,每次请求使用 Math.random(),删除无效的——是你不再维护的代码。一个旋转网关在每次请求时服务器端分配一个新的出口 IP,因此一个端点表现得像一个完整的池。通过 旋转住宅代理,该池跨越 90M+ IP,覆盖 200+ 国家,下面的循环在每次迭代中打印不同的来源,无需轮换逻辑:

import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";

const agent = new HttpsProxyAgent("http://USER:PASS@residentialpremium.quantumproxies.io:8080");
const client = axios.create({ httpAgent: agent, httpsAgent: agent, proxy: false });

for (let i = 0; i < 3; i++) {
  const r = await client.get("https://httpbin.org/ip");
  console.log(r.data.origin); // a different exit IP each time
}

当一个流程跨越多个请求——登录、加入购物车、结账——每次请求的轮换会破坏会话。代理用户名中的粘性会话参数在设定窗口内固定一个出口 IP,然后轮换;相同的端点,只需更改一个字符串。如果你将此扩展到数千个并发请求,请将工作从 axios 循环中移出,并阅读我们的 大规模抓取架构指南 以获取队列和并发预算。

通过代理流式传输响应

一旦代理连接,下载文件或大型 JSON 负载与任何请求相同——设置 responseType: "stream" 并将主体写入磁盘。代理透明地处理传输,因此 200MB 的导出从不在内存中缓冲:

import fs from "node:fs";

const r = await client.get("https://example.com/large.json", {
  responseType: "stream",
});
r.data.pipe(fs.createWriteStream("out.json"));
一个 httpbin.org/ip 页面展示了 Node.js 中的四种代理错误:axios 的 proxy 配置返回你的真实 IP 或卡住、默认导入 agent 时抛出异常、在 axios 中使用 socks5:// 导致协议不匹配,以及 fetch 忽略 HTTP_PROXY。卡片给出了每个问题的修复方法,并有一条提示警告不要同时配置 agent 和 proxy。
大多数 Node.js 代理失败归结为五个错误:axios 的 HTTPS bug、默认代理导入、重复的代理配置、错误的 SOCKS 方案,或期望 fetch 读取环境变量。

常见问题解答

为什么我的 axios 代理无法工作?

最常见的原因是 HTTP 代理后的 HTTPS 目标:axios 的内置 proxy 配置无法打开 CONNECT 隧道,返回你的真实 IP 或挂起。切换到代理代理——将 HttpsProxyAgent 附加到 httpAgent 和 httpsAgent,并设置 proxy: false 以停止 axios 自行处理代理。

axios 支持 SOCKS5 代理吗?

不原生支持——将 socks5:// URL 传递给 proxy 配置会抛出协议不匹配错误。安装 socks-proxy-agent,构建一个 SocksProxyAgent,并将其连接到 httpAgent 和 httpsAgent。使用 socks5h:// 方案,以便 DNS 在代理端解析,而不是从你的机器泄漏。

如何在 Node.js 中使用原生 fetch 代理?

原生 fetch 没有代理选项,并忽略 HTTP_PROXY 环境变量。创建一个 undici ProxyAgent 并将其作为 fetch 调用中的 dispatcher 选项传递,或调用 setGlobalDispatcher() 一次性代理进程中的每个 fetch。Undici 正确处理 HTTPS CONNECT,无需额外配置。

如何使用环境变量设置 axios 代理?

导出 HTTP_PROXY、HTTPS_PROXY 和可选的 NO_PROXY,带完整的代理 URL;axios 会自动读取它们。在请求或实例上设置 proxy: false 以使 axios 忽略环境。记住这只影响 axios——undici 和原生 fetch 不会读取这些变量。

整个故事可以写在卡片上:axios 需要代理和 proxy: false,原生 fetch 需要 undici 调度器,SOCKS 需要自己的代理,轮换应在网关而不是你的循环中进行。管道正确,最后一个变量是 IP 质量——干净的住宅出口通过,而标记的数据中心 IP 在相同代码上得到 403。

获取首次请求即通过的住宅代理