返回博客
指南
Mihnea-Octavian ManolacheLast updated on May 1, 20263 min read

如何在 Node-Fetch 中使用代理:实用指南

如何在 Node-Fetch 中使用代理:实用指南
简而言之:Node-Fetch 没有内置的代理开关,因此你需要通过其 agent 选项将 HTTP、HTTPS 或 SOCKS5 代理集成到请求中。本指南将详细介绍如何在 Node-Fetch 中端到端地使用代理:包括需要身份验证的 HTTP 和 HTTPS 代理、SOCKS5、代理轮换、重试、TLS 特殊情况、故障排除,以及适用于 Node 18+ 原生 fetch 的现代 undici 方案。

如果你曾盯着来自某个目标的 403 错误页面发呆——而你以前还能愉快地抓取该目标——那么你已经知道这篇文章存在的意义了。 掌握在 Node-Fetch 中使用代理的技巧,意味着你的脚本不仅能在笔记本电脑上运行,还能在 CI 环境中——使用不同 IP、位于不同国家、面对真实的反机器人防护体系——依然稳健运行。好消息是:Node-Fetch 的代理使用方法归根结底只需掌握一小部分 API 接口,其余的只是操作层面的衔接。

Node-Fetch 是一款广受欢迎的 Node.js HTTP 客户端,它将浏览器的 window.fetch 风格带到了服务器端。它体积小巧、异步运行且使用体验良好,但它有意不提供 proxy 选项。取而代之的是,它提供了一个 agent 插槽,供您接入外部代理代理。这一设计决策正是下面所有示例背后的核心机制。

本指南不依赖特定提供商,且以代码为先。您将配置 HTTP/HTTPS 代理,发送首个通过代理的请求,安全地添加凭据,切换至 SOCKS5,轮询代理池,添加超时和重试机制,并验证流量是否确实通过代理发送。我们还将介绍使用 undici 的 ProxyAgent的 Node 18+ 替代方案,并提供一份针对入门首日可能遇到的错误的故障排除对照表。

如何在 Node-Fetch 中使用代理:为何需要代理

Node-Fetch 没有内置的代理参数。要将请求路由到代理,你需要传入一个 Node.js http.Agent (或 https.Agent) 实现体传递给 agent 选项 fetch() 调用中传递一个 Node.js(或) 实现。社区包 https-proxy-agentsocks-proxy-agent 实现了该接口,并能将您的流量通过您指定的代理进行隧道传输。

要点:关于 Node-Fetch,您唯一需要了解的是 agent 选项的存在及其支持任何符合规范的代理。其余所有内容——包括 HTTP、HTTPS、SOCKS5、身份验证、TLS 特性及代理轮换——均由代理层处理。这使得您的请求代码在不同提供商间保持一致,并允许您在不重写业务逻辑的情况下切换传输协议。

项目配置与依赖项

你需要一个较新的 Node.js LTS 版本。 node-fetch README 文件中指定的最低版本会随发布版本而变化,因此在锁定 CI 矩阵之前,请务必对照官方 node-fetch 仓库进行核对。当前 LTS 分支上的任何版本通常都是安全的。

请有意选择一个 node-fetch 主版本号。3.x 版本仅支持 ESM,这意味着您需将 "type": "module" 在您的 package.json (或使用 .mjs 文件)并使用 import加载。第2版在CommonJS项目中依然有效,且可用于代理目的。这两个版本的行为基本一致,因此如果您的代码库尚未采用ESM,选择v2也是完全合理的。

请与 Node-Fetch 一起安装代理代理:

# CommonJS friendly
npm install node-fetch@2 https-proxy-agent

# ESM (requires "type": "module" in package.json)
npm install node-fetch https-proxy-agent

通过 HTTP 代理发送首个请求

一旦包已就位,操作流程便十分机械化:构建一个代理 URL,将其传递给 HttpsProxyAgent,并将该代理传递给 fetch()。无论目标是 http:// 还是 https://,因为它会通过 CONNECT.

// proxy-fetch.js (CommonJS, node-fetch v2)
const fetch = require('node-fetch');
const { HttpsProxyAgent } = require('https-proxy-agent');

async function main() {
  const proxyUrl = `http://${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`;
  const agent = new HttpsProxyAgent(proxyUrl);

  const res = await fetch('https://ifconfig.me/all.json', { agent });
  const body = await res.json();
  console.log('Outbound IP:', body.ip_addr);
}

main().catch(console.error);

发布时有几个看似不起眼却至关重要的细节:

  • 请使用解构导入({ HttpsProxyAgent }) 用于 https-proxy-agent v6 及以上版本。不同主要版本间的默认导出结构已发生变化,错误的导入方式是导致 undefined is not a constructor.
  • 针对同一代理的请求应复用代理对象。虽然每次调用新建代理对象也能工作,但这样会失去连接池优势,且每次都需要进行 TLS 握手。
  • 首先访问已知的 IP-echo 端点(ifconfig.me, ident.me,或 ipinfo.io/json)。若未收到代理服务器的 IP 地址响应,请勿继续后续操作;在该基础条件满足前,其他操作均无法正常运行。

对代理服务器进行身份验证

大多数付费代理都需要凭证。惯例是将凭证通过 http://USERNAME:PASSWORD@HOST:PORT 格式,该格式 https-proxy-agent 会为你解析:

const user = encodeURIComponent(process.env.PROXY_USER);
const pass = encodeURIComponent(process.env.PROXY_PASS);
const proxyUrl = `http://${user}:${pass}@${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`;
const agent = new HttpsProxyAgent(proxyUrl);

这里有两个常见陷阱。首先,将凭据硬编码到源文件中会导致其通过 Git 历史记录泄露;请将其保存在环境变量(或密钥管理器)中,并在运行时注入。其次,密码中的特殊字符(@, :, /, #)会悄无声息地破坏 URL 解析,导致出现误导性的 407 Proxy Authentication Required ,而非解析错误。将用户名和密码包裹在 encodeURIComponent() 可以彻底消除这一类错误。

在 Node-Fetch 中使用 SOCKS5 代理

当服务商提供 SOCKS5 端点时,请更换代理代理。Node-Fetch 不关心具体协议;它只会调用你提供的任何代理。安装 socks-proxy-agent:

npm install socks-proxy-agent
const fetch = require('node-fetch');
const { SocksProxyAgent } = require('socks-proxy-agent');

// socks5h:// resolves DNS through the proxy; socks5:// resolves locally.
const proxyUrl = `socks5h://${process.env.PROXY_USER}:${process.env.PROXY_PASS}` +
                 `@${process.env.PROXY_HOST}:${process.env.PROXY_PORT}`;
const agent = new SocksProxyAgent(proxyUrl);

fetch('https://ifconfig.me/all.json', { agent })
  .then(r => r.json())
  .then(console.log);

在抓取时请优先使用 socks5h:// 方案,因为它会通过代理转发 DNS 解析。而普通 socks5:// 方案会在您的机器上解析主机名,这会泄露您的真实 DNS,从而部分抵消了通过代理路由的初衷。

处理 HTTPS 目标和自签名证书

某些代理产品(尤其是拦截式“网页解锁”服务)会向您的客户端展示其自身的 TLS 证书,并重新签署上游响应。Node 默认会因 UNABLE_TO_VERIFY_LEAF_SIGNATURESELF_SIGNED_CERT_IN_CHAIN.

。一个懒惰的解决方法是设置 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'。请勿在生产环境中这样做。这会禁用进程中所有出站请求的证书验证,包括那些你非常希望进行验证的请求。

应将其作用范围限定在代理本身:

const agent = new HttpsProxyAgent(proxyUrl, { rejectUnauthorized: false });

这样可以保持应用程序其余部分的 TLS 安全状态完整。具体构造函数选项的名称及其传播方式在 https-proxy-agent ,因此升级时请重新查阅包的 npm 页面;如果可以固定使用 CA 证书包,请完全避免使用此标志。

轮换代理以实现弹性爬取

许多网站采用基于 IP 的速率限制和机器人检测机制,因此一旦您扩大规模,单个代理 IP 就会被限速或封禁。在代理池中轮换使用可以分散负载,并使每个请求看起来像是来自不同的用户。 在 Node-Fetch 中大规模使用代理时,有两种值得了解的模式:每次请求随机选择,以及确定性的轮询(round-robin)策略。这两种模式都基于您已了解的“每个代理对应一个代理进程”这一核心理念。

每次请求随机选择代理

当您仅关注流量分散时,可随机从代理池中选取,并为每次调用实例化一个新的代理:

const proxies = process.env.PROXY_POOL.split(','); // host:port,host:port,...

function randomAgent() {
  const pick = proxies[Math.floor(Math.random() * proxies.length)];
  return new HttpsProxyAgent(`http://${pick}`);
}

await fetch(targetUrl, { agent: randomAgent() });

如果同一个代理处理了多个连续的请求,请将其代理缓存到一个 Map 以代理 URL 为键进行缓存,从而保持连接复用。

按顺序遍历代理列表

为了实现可重现的调试或简单的轮询行为,请使用计数器遍历列表。顺序轮换还便于归因于每个代理的成功指标,这在开始淘汰失效代理时尤为重要:

let i = 0;
for (const url of urls) {
  const agent = new HttpsProxyAgent(`http://${proxies[i % proxies.length]}`);
  await fetch(url, { agent });
  i++;
}

添加重试、超时和错误处理

通过公共代理的正式生产流量会出现各种奇特的故障:卡死、半开套接字、短暂的 ECONNRESET、突发性 5xx 错误风暴。稳健的配置应结合单次请求超时、带退避机制的有限重试循环,以及在多次失败后淘汰代理的断路器。(关于轮询策略的更多内容,请参阅我们针对网络爬虫代理轮询深度指南。)

async function fetchWithProxy(url, getAgent, opts = {}) {
  const { tries = 3, timeoutMs = 10_000 } = opts;
  let lastErr;

  for (let attempt = 1; attempt <= tries; attempt++) {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), timeoutMs);
    try {
      const res = await fetch(url, { agent: getAgent(), signal: ctrl.signal });
      if (res.ok) return res;
      if (res.status === 407 || res.status === 403) throw new Error(`status ${res.status}`);
    } catch (err) {
      lastErr = err;
      const backoff = 2 ** attempt * 250 + Math.random() * 250;
      await new Promise(r => setTimeout(r, backoff));
    } finally {
      clearTimeout(t);
    }
  }
  throw lastErr ?? new Error('exhausted retries');
}

将每个代理 URL 的失败计数记录在一个小型对象中,当某代理超过阈值(例如一分钟内失败三次)时,将其从池中移除,直到冷却期结束。这条单一规则是大多数教程在讲解如何在 Node-Fetch 中使用代理时所忽略的部分,而正是它能防止少数失效的 IP 地址污染每次重试。将其与 `AbortController` 结合使用,确保卡死的代理永远不会让你的 worker 陷入死锁。

验证代理是否正在被使用

切勿信任未经验证的代理。最简单的测试方法是进行差异比对:分别通过代理和不通过代理各调用一次 IP/地理位置回显端点,然后进行对比。IP 地址和国家/地区必须发生变化。

const direct = await fetch('https://ipinfo.io/json').then(r => r.json());
const proxied = await fetch('https://ipinfo.io/json', { agent }).then(r => r.json());
console.log('direct  :', direct.ip, direct.country);
console.log('proxied :', proxied.ip, proxied.country);

如果两行结果一致,说明你的代理被忽略了,通常是因为导入格式错误或选项键拼写错误。将此检查添加到爬虫的启动探针中,这样无声的直接部署就会发出响亮的失败提示。

Node 18+ 中的原生获取与 Node-Fetch

Node 18 内置了一个全局 fetch (由 undici 提供支持),许多团队已完全放弃了 node-fetch 。但需注意:内置的 fetch 不支持 agent 选项,因此 https-proxy-agent 无法直接接入。其原生等效方案是 undici 的 ProxyAgent,将其设置为全局分发器:

import { ProxyAgent, setGlobalDispatcher } from 'undici';

setGlobalDispatcher(new ProxyAgent(process.env.PROXY_URL));

const res = await fetch('https://ifconfig.me/all.json');
console.log(await res.json());

undici 的代理 API 在各版本中有所变化(URL 中的身份验证、自定义头部、请求范围内的分发器),因此在确定任何配置之前,请先查阅最新的 undici 文档。其思维模型仍与 Node-Fetch 中使用代理的方式完全一致,只是操作在分发器层而非每个请求层面。

排查常见的 Node-Fetch 代理错误

大多数 node-fetch 代理错误可归纳为以下几种情况:

症状

可能原因

解决方法

ECONNREFUSED

代理主机或端口错误,或代理不可用。

Telnet/nc 主机:端口;切换至其他代理。

ETIMEDOUT / 请求挂起

AbortController 超时,或代理正在无声丢弃数据包。

为每次抓取设置请求级超时,并在不同代理上重试。

407 Proxy Authentication Required

凭据缺失或 URL 损坏。

将用户名/密码进行 URL 编码;验证环境变量是否已加载。

SELF_SIGNED_CERT_IN_CHAIN

代理服务器提供了自己的 TLS 证书。

rejectUnauthorized: false (作用域限定),而非全局环境变量。

Cannot find module 'node-fetch'

从 CommonJS 文件导入 v3 ESM。

添加 "type": "module" 或降级至 node-fetch@2.

HttpsProxyAgent is not a constructor

v6+ 的导入格式不正确。

使用 const { HttpsProxyAgent } = require('https-proxy-agent').

当目标在多个正常运行的代理上持续失败时,瓶颈通常在于反机器人指纹识别,而非代理层。此时,无论如何优化 Node-Fetch 中代理的使用方式都无济于事;你需要一个也能处理指纹识别的请求层。

关键要点与后续步骤

Node-Fetch 中使用代理的机制仅是一种选择: agent。选择合适的代理类(HttpsProxyAgent 用于 HTTP/HTTPS, SocksProxyAgent 用于 SOCKS5),向其提供正确编码的 URL,其余工作则包括代理轮换、重试和验证。在此基础上,自然的下一步是构建轮换逻辑分层,并对比其他 Node.js HTTP 客户端。

关键要点

  • Node-Fetch 没有 proxy 选项;你需要通过传递一个 Agent (来自 https-proxy-agentsocks-proxy-agent) 传递给 agent 字段 fetch().
  • 请慎重选择主版本: node-fetch@3 仅限ESM,且需要 "type": "module",而 node-fetch@2 则是兼容 CommonJS 的选项,具有相同的代理行为。
  • 请务必对凭据进行 URL 编码,并从环境变量中读取。A 407 Proxy Authentication Required 通常是解析错误,而非真正的认证失败。
  • 生产级 Node-Fetch 代理代码结合了 AbortController 超时、指数退避以及在多次失败后丢弃失效代理,而不仅仅是围绕 fetch.
  • 在 Node 18 及以上版本中,使用原生 fetchagent 选项无效;请使用 undici 的 ProxyAgent plus setGlobalDispatcher ,并根据最新的 undici 文档重新检查 API 接口。

常见问题

Node-Fetch v3 是否原生支持代理?

不支持。无论是 Node-Fetch v2 还是 v3 均未实现 proxy 选项。这两个主要版本的代理机制完全一致:安装 https-proxy-agent (或 socks-proxy-agent),根据您的代理 URL 构建代理对象,并将其传递给 fetch() 通过 agent 参数传递给。v2与v3之间的唯一区别在于模块系统,v3采用ESM,而v2采用CommonJS。

我可以在多个 fetch 调用中复用同一个 HttpsProxyAgent 吗?

可以,而且你应该这样做。针对每个 (proxyUrl, 目标主机) 组合复用同一个代理,能让底层套接字池保持连接存活,从而省去每次请求的 TLS 握手过程,并在高负载下显著降低延迟。仅当代理 URL 本身发生变化时(例如轮换期间)才构建新的代理。将它们缓存到一个 Map 以代理 URL 为键的缓存中,以同时兼顾复用和轮换。

HttpProxyAgent 和 HttpsProxyAgent 有什么区别?何时需要使用哪一种?

HttpProxyAgent 隧道传输明文 http:// 请求通过代理, CONNECT. HttpsProxyAgent 向代理发送 HTTP CONNECT 请求发送给代理,随后通过 TLS 连接至目标,这是任何 https:// URL 时都需要这种方式。实际上, HttpsProxyAgent 能安全地支持这两种协议,也是默认推荐方案。仅在 HttpProxyAgent 仅在你有特定理由且目标是仅支持 HTTP 的端点时才使用。

为什么我的 Node-Fetch 代理请求返回 407 代理身份验证所需?

407 错误意味着代理未接受您的凭据,但原因通常在于代理上游。最常见的情况是密码中包含特殊字符,导致 URL 解析失败。请将两个字段都包裹在 encodeURIComponent ,并从环境变量中重新加载。之后,请仔细核对您的服务商要求的凭证格式(有些要求在用户名中包含会话令牌、国家代码或会话前缀)。

如何在 Node.js 18+ 中使用内置的 fetch 替代 node-fetch 来配置代理?

Node.js 18+ 中的原生 fetch 功能基于 undici,该库会忽略 agent 选项。安装 undici,根据您的代理 URL 构建一个 ProxyAgent ,并将其注册到 setGlobalDispatcher。此后,所有普通 fetch() 调用都会通过代理路由,无需进一步修改代码。undici API 在各版本中已有所演变,因此在锁定版本前,请对照 undici 的文档核对当前的选项名称。

结论

一旦您掌握了 agent 选项,本指南中的每个方案都只是同一核心理念的细微变体。HTTP、HTTPS 和 SOCKS5 共享相同的请求签名;身份验证本质上是正确的 URL 编码;轮询、重试和跳过失效代理的功能,则封装在 fetch();而 Node 18+ 版本的 undici 路由,则是将这一思维模型映射到全局调度器上的实现。

切勿低估运维层的重要性。免费代理在生产环境中毫无用处:低IP信誉、高停机率以及不可预测的TLS行为将耗尽您的容错预算。一个干净的住宅或数据中心代理池,加上前文提到的超时与重试封装,才能将一段代码片段转化为可部署的爬虫。

如果你不愿亲自处理代理的底层实现,WebScrapingAPI 的 Scraper API 可在单一端点后方自动处理代理轮换、反机器人防护和重试,让你能够保留 node-fetch 代码不变,仅需更换请求层。在此基础上,接下来的自然步骤是分层设计轮换策略,并为你的工作负载选择合适的 Node.js HTTP 客户端,我们的配套指南对此进行了深入探讨。

关于作者
Mihnea-Octavian Manolache, 全栈开发工程师 @ WebScrapingAPI
Mihnea-Octavian Manolache全栈开发工程师

Mihnea-Octavian Manolache 是 WebScrapingAPI 的全栈及 DevOps 工程师,负责开发产品功能并维护确保平台平稳运行的基础设施。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。