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 或更高版本。
: "${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,statusText 为 Proxy Authentication Required,代理的 Proxy-Authenticate 头可以在该响应上读取,响应体为空;同一次导航还触发了一个携带 ERR_TUNNEL_CONNECTION_FAILED 的 requestfailed 事件。因此,一个只 await goto 然后继续执行的脚本会对着一个空页面继续运行。检查 response.ok(),并把 407 当作代理问题而不是目标站点问题:
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 指南 介绍了两者之间的差异。
Puppeteer:在第一次导航之前完成认证
Puppeteer 以标志的形式把代理传给 Chromium,并且只有在调用了 page.authenticate 之后才会应答 407 质询。没有调用它时,记录的运行产生了 net::ERR_INVALID_AUTH_CREDENTIALS,很多人从未把这个错误码与代理联系起来。凭据错误时,goto 以状态 407 完成,响应体是 Chromium 内置的错误页,同时还有一个携带 ERR_HTTP_RESPONSE_CODE_FAILURE 的 requestfailed 事件。
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 目标站点。
一套诊断顺序
- 用相同的网关、目标站点和凭据在 curl 中复现,并读取
http_connect。这能在一分钟内把浏览器的一句话错误转换成代理的实际状态码。 - 对照服务商当前的说明确认代理 scheme(
http://、https://或socks5://)。上表三行ERR_PROXY_CONNECTION_FAILED中有两行是 scheme 写错。 - 把凭据放在工具期望的位置:Playwright 中是
proxy.username和proxy.password,Puppeteer 中是在第一次导航之前调用page.authenticate。在两者中都显式检查response.status() === 407。 - 测试 HTTPS 目标站点,而不是 HTTP 目标站点。HTTP 目标站点会跳过
CONNECT,把隧道问题藏在普通的代理响应背后。 - 检查绕过规则。在原始 Chromium 中,本地和内网目标站点会绕过代理,而公司或环境的代理设置可能覆盖你传入的配置。
- 报告时附上客户端版本、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 端点的文档说明。
参考来源与延伸阅读
本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。