故障排查阅读约 9 分钟

修复 ERR_TUNNEL_CONNECTION_FAILED 错误

解释 ERR_TUNNEL_CONNECTION_FAILED 与 ERR_PROXY_CONNECTION_FAILED,附经验证的失败对照表,并说明如何找回 Chromium 隐藏的 CONNECT 状态码。

本页内容

net::ERR_TUNNEL_CONNECTION_FAILED 表示 Chromium 已经连上了你的代理,用一个 CONNECT 请求要求它打开到 HTTPS 目标站点的隧道,而代理回复的不是成功。net::ERR_PROXY_CONNECTION_FAILED 表示 Chromium 根本没走到那一步:它完全无法与代理建立连接。两个错误码都来自 Chromium 的网络栈,所以 Playwright、Puppeteer 以及任何其他驱动 Chromium 的工具都会报出相同的字符串,通常形如 page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://...

令人沮丧的是 Chromium 省略掉的部分。用 403 拒绝隧道的代理、用 429 对你限速的代理,以及上游宕机返回 502 的代理,产生的都是一模一样的 ERR_TUNNEL_CONNECTION_FAILED。本指南展示在 Playwright、Puppeteer 和 curl 中哪种代理行为会产生哪种错误(结果是在小型受控代理上复现得出的,而不是从论坛帖子中收集的),然后给你一套简短的流程,用来找回浏览器隐藏的状态码。

在 Chrome 或 Edge 中没写任何代码也看到了这个错误?

在普通浏览器窗口中含义完全相同。Chrome 和 Edge 共用 Chromium 的网络栈,并且默认从操作系统读取代理设置,所以这个错误告诉你:某处配置了一个代理,而它拒绝打开隧道(ERR_TUNNEL_CONNECTION_FAILED)或者根本无法连通(ERR_PROXY_CONNECTION_FAILED)。网站本身并没有宕机;浏览器根本没有越过代理。这类配置的常见来源包括:连接时会安装系统代理的 VPN 客户端、代理或 VPN 浏览器扩展、受管设备上的公司策略或 PAC 脚本,或者是手动填入后被遗忘的代理。

按最快找到原因的顺序修复:

  • 打开 chrome://net-internals/#proxy(或 edge://net-internals/#proxy),查看浏览器此刻实际生效的代理设置。如果那里出现了你没有预料到的代理,问题就在这里。
  • 断开 VPN 或禁用代理扩展,重新加载并比较。如果页面能加载,说明是 VPN 或扩展的代理在拒绝隧道或无法连通。
  • 在工作或学校设备上,由代理管理员的规则决定允许访问哪些站点。对被策略封锁的 HTTPS 站点出现 ERR_TUNNEL_CONNECTION_FAILED 是预期行为,只有管理员才能更改。
  • 如果 http:// 页面能加载而 https:// 页面失败,说明代理拒绝了到 443 端口的 CONNECT。这就是下表中的隧道情形,代理自己的状态码会说明原因。

本指南其余部分面向的是在 Playwright 或 Puppeteer 中自行配置代理的开发者,但每个错误字符串在地址栏中的含义完全一样。

两个错误,两个阶段

对于经 HTTP 代理访问的 HTTPS 目标站点,Chromium 会解析代理主机名、与其建立 TCP 连接、发送 CONNECT host:443、等待 2xx 回复,然后才在隧道内开始目标站点的 TLS 握手。这两个错误码标记了该路径上的两个不同位置:

  • ERR_PROXY_CONNECTION_FAILED:代理主机名无法解析、没有任何东西接受 TCP 连接,或者在交换任何 HTTP 之前到代理的连接就失败了。一个常见的自找麻烦的变体是给只讲明文 HTTP 的网关前面写上 https://,这会让 Chromium 尝试与一个期望明文的服务器进行 TLS 握手。
  • ERR_TUNNEL_CONNECTION_FAILED:TCP 连接成功,代理也回应了 CONNECT,但回应的不是成功状态。Chromium 会丢弃代理发送的状态码、原因短语和任何响应体。

你可能看到的其他一切(ERR_CONNECTION_RESETERR_INVALID_HTTP_RESPONSEERR_SOCKS_CONNECTION_FAILED、单纯的导航超时)属于不同的阶段,在后文中介绍。

每种代理失败的表现

下表记录了针对每一种刻意设定的代理行为,各客户端所报告的结果。Playwright 使用其 proxy 启动选项;Puppeteer 使用原始 Chromium 标志(--proxy-server)并用 page.authenticate 提供凭据;curl 使用 -x--proxy-user。三者对接的是同一个 Chromium 153 构建或 Linux 上的 curl 8.18;细节见方法一节。

代理行为Playwright 1.63(Chromium 153)Puppeteer 25(同一 Chromium)curl 8.18
代理主机名无法解析ERR_PROXY_CONNECTION_FAILED相同(5) Could not resolve proxy
代理端口上没有任何监听ERR_PROXY_CONNECTION_FAILED相同(7) Failed to connect
使用 https:// scheme,但代理只讲明文 HTTPERR_PROXY_CONNECTION_FAILED相同(35) TLS connect error
代理接受 TCP 但从不应答导航超时导航超时(28) Connection timed out
代理接受 TCP 后立即关闭ERR_CONNECTION_RESET相同(56) Proxy CONNECT aborted(56) Recv failure: Connection reset by peer
端口有应答,但不是 HTTPERR_INVALID_HTTP_RESPONSE相同(56) Proxy CONNECT aborted
CONNECT 回复 403ERR_TUNNEL_CONNECTION_FAILED相同(7) CONNECT tunnel failed, response 403
CONNECT 回复 429ERR_TUNNEL_CONNECTION_FAILED相同(7) CONNECT tunnel failed, response 429
CONNECT 回复 502 或 503ERR_TUNNEL_CONNECTION_FAILED相同(7) CONNECT tunnel failed, response 502503
CONNECT 回复 407,未配置凭据导航以状态 407 完成,随后该请求出现 ERR_TUNNEL_CONNECTION_FAILEDERR_INVALID_AUTH_CREDENTIALS(7) CONNECT tunnel failed, response 407
CONNECT 回复 407,凭据错误导航以状态 407 完成,随后该请求出现 ERR_TUNNEL_CONNECTION_FAILED导航以状态 407 和 Chromium 的错误页完成,随后出现 ERR_HTTP_RESPONSE_CODE_FAILURE(7) CONNECT tunnel failed, response 407
凭据正确目标站点返回 200200200,http_connect 200
CONNECT 回复 200,随后代理断开隧道ERR_CONNECTION_CLOSEDERR_CONNECTION_RESETERR_CONNECTION_RESET(35) Send failure: Broken pipe
对 HTTP 代理使用 socks5:// schemeERR_SOCKS_CONNECTION_FAILED相同(97) Received invalid version in initial SOCKS5 response
socks5:// 带用户名和密码启动时被拒绝:Browser does not support socks5 proxy authentication未测试未测试
http:// 目标站点,代理以 403 或 407 拒绝无错误:代理自己的 403 或 407 页面就是导航响应相同403 或 407,http_connect 为 000

有三行值得再看一眼。从 403 到 503 的每一个非成功 CONNECT 状态都被压缩成同一个浏览器错误,所以仅凭浏览器无法分辨你是被封锁、被限速,还是遇到了故障的上游。407 是 Chromium 唯一特殊处理的状态码,而 Playwright 和 Puppeteer 呈现它的方式不同。而纯 http:// 目标站点永远不会产生隧道错误,因为根本没有隧道:代理的拒绝会以一个普通响应的形式到达,带着代理的状态码和响应体,这也是为什么同一个脚本在 HTTP 测试页上「能跑」,却在你真正关心的 HTTPS 站点上失败。

用 curl 找回 CONNECT 状态码

curl 保留了 Chromium 丢弃的状态码。使用与你的浏览器脚本相同的网关、目标站点和凭据运行一次有界请求,同时打印目标站点状态码和 CONNECT 状态码。该命令从环境中读取 PROXY_URLPROXY_USERNAMEPROXY_PASSWORD,因此密码永远不会出现在 shell 参数或历史记录中;变量展开需要 curl 8.3 或更高版本。

sh
: "${PROXY_URL:?Set the gateway as http://host:port (no credentials in the URL)}"
CHECK_URL=${CHECK_URL:-https://example.com/}
curl --disable --silent --show-error --output /dev/null \
  --noproxy '' --proxy "$PROXY_URL" --proxy-basic \
  --variable %PROXY_USERNAME --variable %PROXY_PASSWORD \
  --expand-proxy-user '{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}' \
  --max-time 15 \
  --write-out 'destination=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' \
  "$CHECK_URL"

connect=200 加上一个目标站点状态码,表示隧道正常,浏览器的问题出在别处。非零的 connect 值就是 Chromium 隐藏的那个状态码:

  • 407:代理要求凭据,而它没有接受你提供的凭据。检查账户或子用户、百分号编码,以及该网关是按密码还是按允许的来源 IP 认证。407 指南 按这个顺序逐步讲解。
  • 403:代理接受了连接,但拒绝这条隧道。典型原因是目标站点或端口不在服务商允许的列表内、账户受限,或者网关拒绝了某个国家或会话参数。
  • 429:代理侧的并发或速率限制。在 curl -v 的输出中查找 Retry-After 响应头,并在重试前减少并行的浏览器数量。
  • 502、503、504:代理无法到达或选择上游出口。立即重试很少有帮助;记下时间并询问服务商其网关记录了什么。

connect=000 加上非 0 的 curl 退出码,表示失败发生在收到任何 CONNECT 回复之前,对应上表中的 ERR_PROXY_CONNECTION_FAILED 和连接重置各行。当失败是缓慢发生而非立即出现时,超时指南 展示了如何为每个阶段计时。

Playwright:407 不会抛出异常

在没有代理凭据或凭据错误的情况下,记录的运行中 page.goto 并没有 reject。它以一个响应完成:状态为 407,statusTextProxy Authentication Required,代理的 Proxy-Authenticate 头可以在该响应上读取,响应体为空;同一次导航还触发了一个携带 ERR_TUNNEL_CONNECTION_FAILEDrequestfailed 事件。因此,一个只 await goto 然后继续执行的脚本会对着一个空页面继续运行。检查 response.ok(),并把 407 当作代理问题而不是目标站点问题:

js
import { chromium } from 'playwright';

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

const browser = await chromium.launch({
  proxy: {
    server: required('PROXY_URL'),           // http://host:port, no credentials in the URL
    username: required('PROXY_USERNAME'),
    password: required('PROXY_PASSWORD'),
  },
});
try {
  const page = await browser.newPage();
  page.on('requestfailed', (request) => {
    console.error('request failed:', request.failure()?.errorText, request.url());
  });
  const response = await page.goto('https://example.com/', { waitUntil: 'domcontentloaded' });
  if (!response) throw new Error('No navigation response');
  if (response.status() === 407) throw new Error('Proxy rejected the credentials (407)');
  if (!response.ok()) throw new Error('Destination answered ' + response.status());
} finally {
  await browser.close();
}

把代理凭据放在 proxy.usernameproxy.password 中。在记录的运行中,通过上下文的 httpCredentials 选项提供的凭据同样满足了代理质询,因为 Playwright 会用任一来源来应答 Chromium 的认证请求。不要依赖这一点:httpCredentials 是为目标站点准备的,把它复用给代理会把站点登录信息发送给代理运营方。

再补充记录中的两个 Playwright 细节。Playwright 会自行把 <-loopback> 加入绕过列表,因此 localhost127.0.0.1 上的目标站点会经过代理;原始 Chromium 则相反(见 Puppeteer 一节)。另外,带用户名和密码的 socks5:// 服务器会在浏览器启动之前就被拒绝,提示 Browser does not support socks5 proxy authentication。Chromium 不支持 SOCKS5 认证,所以解决办法是改用带 Basic 认证或 IP 白名单的 HTTP 网关,而不是换一种 SOCKS 设置。HTTP 与 SOCKS5 指南 介绍了两者之间的差异。

Puppeteer:在第一次导航之前完成认证

Puppeteer 以标志的形式把代理传给 Chromium,并且只有在调用了 page.authenticate 之后才会应答 407 质询。没有调用它时,记录的运行产生了 net::ERR_INVALID_AUTH_CREDENTIALS,很多人从未把这个错误码与代理联系起来。凭据错误时,goto 以状态 407 完成,响应体是 Chromium 内置的错误页,同时还有一个携带 ERR_HTTP_RESPONSE_CODE_FAILURErequestfailed 事件。

js
import puppeteer from 'puppeteer';

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

const browser = await puppeteer.launch({
  args: [
    '--proxy-server=' + required('PROXY_URL'),   // http://host:port
    '--proxy-bypass-list=<-loopback>',          // only if local destinations must use the proxy
  ],
});
try {
  const page = await browser.newPage();
  await page.authenticate({
    username: required('PROXY_USERNAME'),
    password: required('PROXY_PASSWORD'),
  });
  page.on('requestfailed', (request) => {
    console.error('request failed:', request.failure()?.errorText, request.url());
  });
  const response = await page.goto('https://example.com/', { waitUntil: 'domcontentloaded' });
  if (!response) throw new Error('No navigation response');
  if (response.status() === 407) throw new Error('Proxy rejected the credentials (407)');
  if (!response.ok()) throw new Error('Destination answered ' + response.status());
} finally {
  await browser.close();
}

这里有两个 Chromium 默认行为很重要。第一,除非绕过列表中包含 <-loopback>,否则 Chromium 会对 localhost127.0.0.1[::1] 绕过代理。在这次运行中,一个指向拒绝所有隧道的代理的 Puppeteer 脚本仍然以状态 200 加载了本地 HTTPS 页面,因为代理根本没有被使用。针对本地服务器「通过」的测试对代理没有任何证明力。第二,用 --proxy-server 启动的原始 Chromium 也会把自己的后台流量经由代理发送:记录的代理日志显示,在页面导航之前有一个发往 clients2.google.comGET 和一个发往 update.googleapis.com:443CONNECT。预计会在服务商日志和计量流量中看到这些主机名,不要把它们的失败误认为是你的导航失败。同一次运行中,Playwright 的启动标志没有产生这类流量。

不属于隧道错误的错误

  • 连接后立刻出现 ERR_CONNECTION_RESETERR_CONNECTION_CLOSED:代理接受了 TCP 连接然后断开,要么在应答 CONNECT 之前,要么在返回 200 之后立即断开。服务商在出口不可用或在套接字层面限速时会这样做。对于同样的行为,curl 报告 Proxy CONNECT abortedSend failure: Broken pipe
  • ERR_INVALID_HTTP_RESPONSE:那个端口上有东西应答,但不是 HTTP。通常是端口号属于 SOCKS 监听器、仅 TLS 的监听器或其他服务。
  • ERR_SOCKS_CONNECTION_FAILED:你配置了 socks5://,而服务器讲的是 HTTP。先把 scheme 改成与网关文档中记录的协议一致,再改其他任何东西。
  • 没有 net 错误的导航超时:代理接受了连接但从不回复。加大超时没有用;用 curl 验证端口和协议,在同样的情况下它会以 (28) 超时。
  • 在 curl 中正常的网关出现 ERR_PROXY_CONNECTION_FAILED:比较 scheme。Chromium 会对 https:// 代理尝试 TLS,而大多数网关是纯 http:// 端点,在隧道内承载 HTTPS 目标站点。

一套诊断顺序

  1. 用相同的网关、目标站点和凭据在 curl 中复现,并读取 http_connect。这能在一分钟内把浏览器的一句话错误转换成代理的实际状态码。
  2. 对照服务商当前的说明确认代理 scheme(http://https://socks5://)。上表三行 ERR_PROXY_CONNECTION_FAILED 中有两行是 scheme 写错。
  3. 把凭据放在工具期望的位置:Playwright 中是 proxy.usernameproxy.password,Puppeteer 中是在第一次导航之前调用 page.authenticate。在两者中都显式检查 response.status() === 407
  4. 测试 HTTPS 目标站点,而不是 HTTP 目标站点。HTTP 目标站点会跳过 CONNECT,把隧道问题藏在普通的代理响应背后。
  5. 检查绕过规则。在原始 Chromium 中,本地和内网目标站点会绕过代理,而公司或环境的代理设置可能覆盖你传入的配置。
  6. 报告时附上客户端版本、UTC 时间、网关主机和端口、目标站点主机名以及 curl 的 http_connect 值。不要包含密码、带凭据的 URL 和原始跟踪记录;407 指南 提供了一份脱敏报告模板。

方法与下载

ipvolt 于 2026 年 9 月 17 日在一台 Linux 主机上完成了此次复现,使用 Node.js 24.20.0、Playwright 1.63.0(Chromium 153.0.8010.12,无头模式)、驱动同一 Chromium 构建的 puppeteer-core 25.11.0,以及 curl 8.18.0。每个「代理」都是 127.0.0.1 上一个只有单一行为的小型 Node.js 服务器;目标站点是一个使用一次性自签名证书的本地 HTTPS 服务器,因此浏览器用例忽略了目标站点的证书错误,这不影响 Chromium 与代理的交互方式。未测试 Firefox 和 WebKit,也没有涉及任何商业网关:该表描述的是 Chromium 对某种代理行为的反应,而不是任何服务商的策略。

实验归档 包含实验脚本包清单README记录结果,其中包括代理侧日志,显示每个客户端是否到达了代理以及凭据是否送达。它只在回环地址上运行,不需要代理账户。

如果你想在 ipvolt 开放访问时收到通知,请加入早期访问名单。开放时只发一封邮件。别无其他。这些示例是客户端诊断,并不是对某个可用 ipvolt 端点的文档说明。

参考来源与延伸阅读

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