# 修复 ERR_TUNNEL_CONNECTION_FAILED 错误

Source: https://ipvolt.com/zh/guides/fix-err-tunnel-connection-failed
Markdown: https://ipvolt.com/zh/guides/fix-err-tunnel-connection-failed.md
Language: zh-CN

[ipvolt 首页](https://ipvolt.com/zh.md) / [指南](https://ipvolt.com/zh/guides.md) / 修复 ERR_TUNNEL_CONNECTION_FAILED 错误

故障排查
审校于: 2026-09-17
发布于: 2026-09-17
阅读约 9 分钟
作者： ipvolt

解释 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_RESET`、`ERR_INVALID_HTTP_RESPONSE`、`ERR_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，但代理只讲明文 HTTP | `ERR_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` |
| 端口有应答，但不是 HTTP | `ERR_INVALID_HTTP_RESPONSE` | 相同 | `(56) Proxy CONNECT aborted` |
| CONNECT 回复 403 | `ERR_TUNNEL_CONNECTION_FAILED` | 相同 | `(7) CONNECT tunnel failed, response 403` |
| CONNECT 回复 429 | `ERR_TUNNEL_CONNECTION_FAILED` | 相同 | `(7) CONNECT tunnel failed, response 429` |
| CONNECT 回复 502 或 503 | `ERR_TUNNEL_CONNECTION_FAILED` | 相同 | `(7) CONNECT tunnel failed, response 502` 或 `503` |
| CONNECT 回复 407，未配置凭据 | 导航以状态 407 完成，随后该请求出现 `ERR_TUNNEL_CONNECTION_FAILED` | `ERR_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` |
| 凭据正确 | 目标站点返回 200 | 200 | 200，`http_connect` 200 |
| CONNECT 回复 200，随后代理断开隧道 | `ERR_CONNECTION_CLOSED` 或 `ERR_CONNECTION_RESET` | `ERR_CONNECTION_RESET` | `(35) Send failure: Broken pipe` |
| 对 HTTP 代理使用 `socks5://` scheme | `ERR_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_URL`、`PROXY_USERNAME` 和 `PROXY_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 指南](/zh/guides/fix-proxy-error-407) 按这个顺序逐步讲解。
- **403**：代理接受了连接，但拒绝这条隧道。典型原因是目标站点或端口不在服务商允许的列表内、账户受限，或者网关拒绝了某个国家或会话参数。
- **429**：代理侧的并发或速率限制。在 `curl -v` 的输出中查找 `Retry-After` 响应头，并在重试前减少并行的浏览器数量。
- **502、503、504**：代理无法到达或选择上游出口。立即重试很少有帮助；记下时间并询问服务商其网关记录了什么。

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

## Playwright：407 不会抛出异常

在没有代理凭据或凭据错误的情况下，记录的运行中 `page.goto` 并没有 reject。它以一个响应完成：状态为 407，`statusText` 为 `Proxy Authentication Required`，代理的 `Proxy-Authenticate` 头可以在该响应上读取，响应体为空；同一次导航还触发了一个携带 `ERR_TUNNEL_CONNECTION_FAILED` 的 `requestfailed` 事件。因此，一个只 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.username` 和 `proxy.password` 中。在记录的运行中，通过上下文的 `httpCredentials` 选项提供的凭据同样满足了代理质询，因为 Playwright 会用任一来源来应答 Chromium 的认证请求。不要依赖这一点：`httpCredentials` 是为目标站点准备的，把它复用给代理会把站点登录信息发送给代理运营方。

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

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

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

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

## 不属于隧道错误的错误

- **连接后立刻出现 `ERR_CONNECTION_RESET` 或 `ERR_CONNECTION_CLOSED`**：代理接受了 TCP 连接然后断开，要么在应答 `CONNECT` 之前，要么在返回 200 之后立即断开。服务商在出口不可用或在套接字层面限速时会这样做。对于同样的行为，curl 报告 `Proxy CONNECT aborted` 或 `Send 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.username` 和 `proxy.password`，Puppeteer 中是在第一次导航之前调用 `page.authenticate`。在两者中都显式检查 `response.status() === 407`。
4. 测试 HTTPS 目标站点，而不是 HTTP 目标站点。HTTP 目标站点会跳过 `CONNECT`，把隧道问题藏在普通的代理响应背后。
5. 检查绕过规则。在原始 Chromium 中，本地和内网目标站点会绕过代理，而公司或环境的代理设置可能覆盖你传入的配置。
6. 报告时附上客户端版本、UTC 时间、网关主机和端口、目标站点主机名以及 curl 的 `http_connect` 值。不要包含密码、带凭据的 URL 和原始跟踪记录；[407 指南](/zh/guides/fix-proxy-error-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 对某种代理行为的反应，而不是任何服务商的策略。

[实验归档](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/chromium-proxy-tunnel-errors.zip) 包含[实验脚本](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/tunnel-errors-lab.mjs)、[包清单](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/package.json)、[README](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/README.md) 和[记录结果](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/results.json)，其中包括代理侧日志，显示每个客户端是否到达了代理以及凭据是否送达。它只在回环地址上运行，不需要代理账户。

如果你想在 ipvolt 开放访问时收到通知，请[加入早期访问名单](https://ipvolt.com/#waitlist-closing)。开放时只发一封邮件。别无其他。这些示例是客户端诊断，并不是对某个可用 ipvolt 端点的文档说明。

## 参考来源与延伸阅读

- [Chromium network error list (net_error_list.h)](https://chromium.googlesource.com/chromium/src/+/main/net/base/net_error_list.h)
- [Chromium proxy support and bypass rules (proxy.md)](https://chromium.googlesource.com/chromium/src/+/main/net/docs/proxy.md)
- [Playwright: HTTP proxy configuration](https://playwright.dev/docs/network#http-proxy)
- [Puppeteer: page.authenticate](https://pptr.dev/api/puppeteer.page.authenticate)
- [curl: --write-out variables including http_connect](https://curl.se/docs/manpage.html#-w)
- [RFC 9110: the CONNECT method](https://www.rfc-editor.org/rfc/rfc9110.html#name-connect)
- [RFC 9110: 407 Proxy Authentication Required](https://www.rfc-editor.org/rfc/rfc9110.html#name-407-proxy-authentication-req)

## 相关指南

- [在 Playwright 中配置代理](https://ipvolt.com/zh/guides/playwright-proxy-setup.md)
- [不靠猜测修复代理错误 407](https://ipvolt.com/zh/guides/fix-proxy-error-407.md)
- [一次一个阶段地排查代理超时](https://ipvolt.com/zh/guides/proxy-timeout-troubleshooting.md)

## 关于 ipvolt

示例使用通用的代理设置，并附上原始技术文档链接。具体产品的行为请向你的服务商确认。ipvolt 仍在开发中。

[阅读英文原文](https://ipvolt.com/guides/fix-err-tunnel-connection-failed.md)

## 了解何时开放体验。

ipvolt · 开发中

我们正在为开发者和数据团队打造代理基础设施。加入意向名单，ipvolt 就绪时第一时间收到通知。

开放体验时仅发一封通知邮件，不发送其他邮件。

[申请抢先体验](https://ipvolt.com/zh/guides/fix-err-tunnel-connection-failed#waitlist-closing)

[隐私政策](https://ipvolt.com/privacy)

