Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

如何在 Node.js 中使用 SuperAgent 配合代理服务器

SuperAgent 没有内置的代理选项。您要么添加一个扩展,要么提供一个 HTTP 代理,但这两种方法都会因命名冲突而变得复杂:SuperAgent 中的 `.agent()` 实际上已经代表了完全不同的含义。 这种冲突会导致一种特定的混淆,即用户误以为自己配置了代理,实际上却配置了 Cookie 存储器。 本指南将介绍这两种方法、相关术语,以及用于判断哪种配置实际生效的验证方法。

我们是 Geonode,主要销售代理服务,因此本文是一份关于如何将我们的产品与特定客户端配合使用的指南。即使您跳过其余内容,这一句也值得阅读:配置完成后请务必验证出口地址,因为无效的代理设置根本不会触发任何错误提示。 SuperAgent 会很“乐意”地直接发送您的请求,返回 200 状态码,并且不会给您任何提示表明代理已被绕过。验证部分只有四行代码,却能区分“确知”与“臆测”。

另请注意,SuperAgent 在浏览器中同样有效,但上述内容均不适用——无法通过 JavaScript 指示浏览器使用代理,因此本文所述内容仅适用于 Node 环境。

术语问题

首先澄清这一点,因为它会导致实际错误。

在 SuperAgent 中,.agent() 不带任何参数 会创建一个会保存 Cookie 的 SuperAgent 副本。文档 中明确指出: “在 Node 中,SuperAgent 默认不保存 Cookie,但你可以使用 .agent() 方法创建一个会保存 Cookie 的 SuperAgent 副本。每个副本都有独立的 Cookie 存储空间。”

const agent = request.agent();
await agent.post("/login").send({ user, pass });
await agent.get("/cookied-page");   // session cookie carried over

该代理还带有默认设置:“在代理上调用的常规请求方法将作为该代理所发所有请求的默认方法。”

与此同时,带参数的 .agent(httpAgent) 会为请求设置 Node 的 http.Agent,而代理支持功能正是在此处实现的。

方法名称相同,但功能却毫无关联,唯一的区别仅在于是否传递参数。如果你在同一会话中既查阅了关于 SuperAgent 代理的文档,又查阅了关于代理代理的文档,那么在编写任何代码之前,有必要先厘清这一点。

方案一:代理代理

这是首选的方法,原因在于维护便利性。

import request from "superagent";
import { HttpsProxyAgent } from "https-proxy-agent";

const agent = new HttpsProxyAgent("http://myuser:mypass@proxy.example.com:9000");

const res = await request
  .get("https://api.example.com/items")
  .agent(agent);

对于 SOCKS,请更换该软件包:

import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://proxy.example.com:1080");

请注意使用 socks5h

而不是 socks5

。h

变体会在代理端而非本地解析主机名,这可以防止在流量经由其他地方传输时,DNS 查询发送到您自己的解析器——这种泄漏会悄无声息地抵消使用代理进行地理定位的意义。

此外,proxy-agent

会处理 URL 指定的任何协议,当代理来自配置时,这非常有用:

import { ProxyAgent } from "proxy-agent";
const agent = new ProxyAgent();   // reads http_proxy / https_proxy / no_proxy

**为何选择这些包而非 SuperAgent 扩展:**这三个包均处于积极维护中。经 2026 年 9 月在 npm 注册表中核查,proxy-agent

版本为 8.0.2,https-proxy-agent

版本为 9.1.0,socks-proxy-agent

版本为 10.1.0,均发布于 2026 年 6 月。

方案二:superagent-proxy

这款专为该用途设计的扩展及其注意事项。

import request from "superagent";
import superagentProxy from "superagent-proxy";

superagentProxy(request);

const res = await request
  .get("https://api.example.com/items")
  .proxy("http://myuser:mypass@proxy.example.com:9000");

其 README 文件将其描述为“通过 .proxy(uri) 函数扩展了 superagent 的 Request 类”,并指出“它由 proxy-agent 模块提供支持”。

这种 API 比直接传递代理更优雅。.proxy(uri) 调用在链式调用中更易读,且支持“HTTP、HTTPS 或 SOCKS”格式的 URI,并将协议选择委托给 proxy-agent。

需要注意的是发布日期。 superagent-proxy 当前版本为 3.0.0,发布于 2021 年 9 月——截至本文撰写时已有大约五年历史,而其底层依赖项 proxy-agent 却一直在持续更新。该扩展虽未被废弃且功能正常,但它只是一个薄包装器,在被包装的组件不断演进的同时,自身却停滞不前。

实际影响是:如果你已经在使用它且运行正常,则无需急于更新。对于新代码,直接使用代理代理只需多写一行代码,就能从依赖树中移除一个过时的层级——而且由于该扩展正是围绕该代理构建的封装,你除了语法上的便利外,没有任何损失。

验证是否真正生效

这四行代码至关重要。

const res = await request
  .get("https://api.ipify.org?format=json")
  .agent(agent);

console.log(res.body);

分别在启用代理和禁用代理的情况下运行该代码。**如果地址没有变化,说明代理未被纳入路径。**此时,SuperAgent 不会返回任何错误、警告或请求失败——流量会直接通过。

配置通常无效的三个原因:

您在调用 .agent() 时未传入参数,这会创建一个通过 Cookie 保持状态的 SuperAgent 实例,而非设置 HTTP 代理。这是术语上的陷阱,且会导致上述症状。

您将代理应用到了错误的请求上。 SuperAgent 的链式处理是按请求进行的,因此在一个调用中设置的代理不会应用于下一个请求。为了确保行为一致,请将请求创建封装在函数中。

代理类型与目标不匹配。 HttpsProxyAgent 用于处理 HTTPS 目标;而普通 HTTP 目标可能需要 HTTP 变体。proxy-agent 会自动为您选择合适的类型,从而规避此问题。

对于基于地理位置的代理,仅检查地址是不够的。 通过结果验证 — 请求一些真正因地区而异的内容,并确认响应发生了变化。如果查询服务报告了正确的国家,而您的 API 却返回了您所在地区的数据,这意味着定位并未到达关键位置,这就是我们在 为什么测试代理很重要 中描述的“沉默失败”模式。

通过代理时的超时设置

SuperAgent 的超时机制表现尤为出色,当代理导致延迟时,值得妥善利用这一功能。

文档中描述了两个设置。req.timeout({deadline: ms})

— 或 req.timeout(ms)

— “为整个请求(包括所有上传、重定向及服务器处理时间)设定完成时限。 如果在此时间内未能完全下载响应,请求将被中止。”而 req.timeout({response: ms})

“设置了等待服务器发送第一个字节的最大时间,但并不限制整个下载过程的持续时间。”

文档中关于时间设置的建议与代理请求直接相关:“响应超时时间应比服务器响应所需的时间至少多出几秒钟,因为它还包括进行 DNS 查询、建立 TCP/IP 和 TLS 连接,以及上传请求数据所需的时间。”

通过代理,上述每个阶段的耗时都会增加。住宅出口节点会给每次请求带来实际的延迟,这源于距离而非网络故障。

const res = await request
  .get("https://api.example.com/items")
  .agent(agent)
  .timeout({ response: 15000, deadline: 60000 });

文档建议同时使用这两项设置,原因与其他地方一致:响应超时用于捕获始终不响应的服务器,而截止时间则用于捕获那些响应后却断断续续传输数据的服务器。 仅靠其中一种无法覆盖这两种情况。

请根据通过代理的实际测量数据来设置参数,而非凭习惯,否则会引发看似代理故障的错误,而实际上只是你未预留的延迟所致。

错误处理

SuperAgent 的默认行为与大多数客户端不同,这一点在此处尤为重要。

文档中明确指出:“SuperAgent 默认将 4xx 和 5xx 响应(以及未处理的 3xx 响应)视为错误”。 文档还补充道:“可通过 err.status 获取此状态信息”,并且此类错误“还包含一个 err.response 字段”。

因此,代理认证失败会以拒绝响应的形式返回,而非普通响应:

try {
  const res = await request.get(url).agent(agent).timeout({ deadline: 30000 });
  return res.body;
} catch (err) {
  if (err.status === 407) throw new Error("Proxy rejected credentials");
  if (err.status === 401) throw new Error("Target requires authentication");
  if (!err.status) throw new Error(`Network error: ${err.code} ${err.message}`);
  throw err;
}

407 与 401 之间的区别值得特别关注。 407 表示代理阻止了你,且从未到达目标;401 表示代理正常工作,但目标要求凭据。这两者涉及不同的方,需要不同的解决方法,而当它们都作为抛出的错误出现时,极易混淆。

err.status 为空的错误意味着根本没有收到任何 HTTP 响应,这表明问题出在连接上,而非任何认证环节。ECONNREFUSED 表示代理地址上无人监听;ETIMEDOUT 表示数据包在传输过程中丢失。

若要将某些错误状态视为成功——例如将 404 视为数据而非失败——请使用 .ok():

.ok(res => res.status < 500)

重试,需谨慎

SuperAgent 内置了重试功能,但有一项值得遵守的限制,相关说明详见文档。

const res = await request.get(url).agent(agent).retry(2);

文档中解释道,.retry()“将在请求因暂时的故障或不稳定的网络连接而失败时自动重试”,该方法接受一个可选的重试次数(默认值为 1)以及一个“在每次重试前”调用的回调函数。 该回调函数“可返回 true/false 来控制是否应重试请求(但始终会应用最大重试次数)”。

而文档中明确指出的限制是:.retry()“仅适用于 幂等 的请求”。

通过代理时,这一点比平时更为重要,原因很具体。超时并不意味着失败——请求可能已到达目标并成功,但响应在返回途中丢失了。由于存在额外的跳转,这种情况就可能发生。 在此情况下重试 POST 请求可能会导致写入操作重复,无论如何配置重试机制都无法确保其安全性。若操作结果至关重要,且 API 提供幂等性密钥,请务必使用该密钥。

回调函数也是避免对认证失败进行重试的合适位置,因为凭证错误导致的 407 状态码会在每次尝试时都返回 407:

.retry(3, (err, res) => {
  if (res?.status === 407 || res?.status === 401) return false;
  return true;
})

值得编写的封装器

SuperAgent 的链式调用是按请求进行的,这意味着在那个至关重要的调用中,很容易忘记配置代理。通过封装请求的创建可以解决这个问题,并为你提供一个放置其余默认设置的地方。

import request from "superagent";
import { ProxyAgent } from "proxy-agent";

const agent = process.env.PROXY_URL ? new ProxyAgent(process.env.PROXY_URL) : undefined;

const UA = "AcmeBot/1.0 (+https://acme.example.com/bot)";

function req(method, url) {
  const r = request[method](url)
    .set("User-Agent", UA)
    .timeout({ response: 15000, deadline: 60000 })
    .retry(2, (err, res) => {
      if (res?.status === 407 || res?.status === 401) return false;
      if (res?.status === 429) return false;   // honour the rate limit instead
      return true;
    });
  return agent ? r.agent(agent) : r;
}

export const get = url => req("get", url);
export const post = url => req("post", url);

export async function verifyExit() {
  const res = await get("https://api.ipify.org?format=json");
  console.log(`Exit address: ${res.body.ip}`);
  return res.body.ip;
}

其中的五项选择都是经过深思熟虑的。

代理是可选的,且来自环境变量。 如果未设置 PROXY_URL,代理将默认为 undefined,请求将直接发送,这使得本地开发和生产环境的行为可预测,且无需在应用程序代码中进行分支处理。源代码中不会出现任何凭据。

**若您偏好环境驱动的配置,未传入构造函数参数的ProxyAgent 将解析为 http_proxy 及其变体;**而显式传递 URL 则能明确“真实来源”,这通常更为重要。

**用户代理信息真实可靠,并包含联系 URL。**这无需任何成本,且当网站运营方注意到您时,会改变后续的处理方式。

重试机制会排除那些重试毫无意义或不礼貌的状态码。 因凭证错误导致的 407 状态码每次都会是 407;429 状态码是要求放缓请求的指令,若在此状态下继续重试,会将临时限制转化为更长时间的限制。

verifyExit() 被导出并在启动时被调用。 仅需六行代码,就能将一个被默默绕过的代理转变为日志中的一行记录——这是每个客户端中代理工作的唯一核心主题,也是没有任何库能为你代劳的一件事。

.connect() 并非代理

值得指出的是,它虽然看起来像代理,但实际上并非如此。

SuperAgent 提供了一个 .connect() 方法,根据文档说明,该方法可实现“忽略 DNS 解析并将所有请求直接发送到特定 IP 地址”。它支持映射功能,包括 * 的备用方案:

const res = await request.get("http://redir.example.com:555")
  .connect({
    "redir.example.com": "127.0.0.1",
    "www.example.com": false,
    "mapped.example.com": { host: "127.0.0.1", port: 8080 },
    "*": "proxy.example.com",
  });

文档指出“请求将保留其Host标头及其原始值”,并且.connect(undefined)可关闭此功能。

这属于主机重定向,而非代理。它仅改变了连接的目标地址,而请求本身保持不变——既没有CONNECT隧道,也没有代理协议,更没有代理认证。 该功能用于测试,文档将其归类在“在 localhost 上测试”部分,这有其充分的理由。

官方示例中的 "*": "proxy.example.com" 这一行正是造成混淆的根源。请使用 .connect() 将请求指向本地测试服务器;若需实际使用代理,请使用代理代理(agent)。

大家还问

如何在 SuperAgent 中使用代理?

将代理代理传递给 .agent():创建一个包含代理 URL 的 HttpsProxyAgent 或 SocksProxyAgent,并将其传递给请求。或者使用 superagent-proxy 扩展,该扩展添加了 .proxy(uri) 方法——不过该包自 2021 年以来尚未发布新版本。

带参数和不带参数的 .agent() 有什么区别?

不带参数时,它会创建一个具有独立 JAR 文件和默认选项的 SuperAgent 实例,该实例会保留 Cookie。带参数时,它会为该请求设置 Node 的 http.Agent,这就是实现代理支持的方式。名称的相似性确实容易造成混淆。

superagent-proxy 是否仍在维护中?

它并未被废弃,但 3.0.0 版本发布于 2021 年 9 月,而其底层依赖项 proxy-agent 仍在持续发布新版本——最近一次是在 2026 年 6 月。对于新代码,直接使用代理代理虽然多出一行代码,但可以避免使用过时的封装器。

为什么我的 SuperAgent 代理无法正常工作?

最常见的原因是调用 .agent() 时未传入参数,这会创建一个 Cookie 存储器,而非设置代理。还请检查代理是否已应用于正确的请求,因为 SuperAgent 的链式调用是按每次调用分别处理的。可通过请求一个会返回您 IP 地址的服务来验证——如果代理被绕过,则不会产生错误。

SuperAgent 是否支持 HTTP_PROXY 环境变量?

它本身不支持。但 proxy-agent 包会读取 http_proxy、https_proxy 和 no_proxy,因此通过无参数调用 ProxyAgent() 并将其传递给 .agent(),即可实现基于环境变量的行为。

如何为代理请求设置超时?

同时使用以下两个设置:.timeout({ response: 15000, deadline: 60000 })。响应超时限制了等待第一个字节的时间,而截止时间则限制了整个请求。请根据通过代理测得的数据来调整这些值,因为家庭网络出口会给 DNS、连接和 TLS 阶段都增加实际延迟。

如何区分代理错误和目标错误?

通过状态码判断。SuperAgent 将 4xx 和 5xx 状态码视为错误,因此请捕获并解析 err.status — 407 表示代理拒绝了请求且从未连接到目标,而 401 表示代理正常工作,但目标要求提供凭据。 如果完全没有 err.status,则表示未收到任何 HTTP 响应。

能否将 .connect() 用作代理?

不能。它会将请求重定向到特定 IP 地址,同时保留原始的 Host 头部,这属于用于测试的主机映射,而非代理功能。该方式不建立隧道,不使用代理协议,也不进行身份验证。如需真正的代理功能,请使用代理程序。

总结

SuperAgent 本身不提供代理选项,因此只能选择代理程序或扩展程序——而代理程序是更好的默认选择,因为无论哪种情况,实际执行工作的都是那些经过维护的软件包。

术语是主要陷阱。不带参数的 .agent() 会返回一个 Cookie 容器;而 .agent(something) 则会设置一个 HTTP 代理。人们配置前者后,看到请求成功,便认为代理正在工作。却无人纠正这种误解,因为根据定义,被绕过的代理会无声地失败。

因此,养成验证的习惯至关重要。分别启用和禁用代理,向返回您IP地址的服务发送请求,并确认响应内容发生变化。对于基于地理位置的任务,请进一步确认不同地区的区域性内容确实存在差异——IP地址只是最简单且信息量最少的部分。

然后设置两项超时,根据 err.status 进行分支处理,确保 407 和 401 状态码会触发不同的错误信息,并避免将 .retry() 用于任何非幂等操作。通过代理时会增加一个额外的中转环节,成功的请求可能在此丢失响应;这种情况下,重试只会导致请求重复,而非恢复。