# Fix ERR_TUNNEL_CONNECTION_FAILED in Playwright and Puppeteer

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

[Home](https://ipvolt.com/index.md) / [Guides](https://ipvolt.com/guides.md) / [Fix ERR_TUNNEL_CONNECTION_FAILED in Playwright and Puppeteer](https://ipvolt.com/guides/fix-err-tunnel-connection-failed.md)

Category: Troubleshooting
Reviewed: 2026-09-17
Published: 2026-09-17
Reading time: 9 minutes
Author: ipvolt

ERR_TUNNEL_CONNECTION_FAILED and ERR_PROXY_CONNECTION_FAILED explained with a verified proxy failure matrix, plus how to recover the CONNECT status Chromium hides.

`net::ERR_TUNNEL_CONNECTION_FAILED` means Chromium reached your proxy, asked it to open a tunnel to an HTTPS destination with a `CONNECT` request, and the proxy answered with something other than success. `net::ERR_PROXY_CONNECTION_FAILED` means Chromium never got that far: it could not establish a connection to the proxy at all. Both codes come from Chromium's network stack, so Playwright, Puppeteer and any other tool driving Chromium report the same strings, usually as `page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://...`.

The frustrating part is what Chromium leaves out. A proxy that refuses a tunnel with 403, one that is rate limiting you with 429, and one whose upstream is down with 502 all produce the identical `ERR_TUNNEL_CONNECTION_FAILED`. This guide shows which proxy behaviour produces which error in Playwright, Puppeteer and curl, reproduced against small controlled proxies rather than collected from forum threads, and then gives you a short procedure to recover the status the browser hides.

## Seeing this in Chrome or Edge without any code?

The meaning is the same in a normal browser window. Chrome and Edge share Chromium's network stack, and by default they take their proxy settings from the operating system, so the error tells you that a proxy is configured somewhere and that it refused to open the tunnel (`ERR_TUNNEL_CONNECTION_FAILED`) or could not be reached at all (`ERR_PROXY_CONNECTION_FAILED`). The website itself is not down; the browser never got past the proxy. The usual sources of that configuration are a VPN client that installs a system proxy while connected, a proxy or VPN browser extension, a company policy or PAC script on a managed device, or a proxy that was entered by hand and forgotten.

How to fix it, in the order that finds the cause fastest:

- Open `chrome://net-internals/#proxy` (or `edge://net-internals/#proxy`) to see the effective proxy settings the browser is using right now. If you did not expect a proxy there, that is the problem.
- Disconnect the VPN or disable proxy extensions, reload, and compare. If the page loads, the VPN or extension's proxy was refusing the tunnel or was unreachable.
- On a work or school device, the proxy administrator's rules decide which sites are allowed. `ERR_TUNNEL_CONNECTION_FAILED` on an HTTPS site that is blocked by policy is expected behaviour, and only the administrator can change it.
- If `http://` pages load but `https://` pages fail, the proxy is refusing `CONNECT` to port 443. That is the tunnel case in the matrix below, and the proxy's own status code says why.

The rest of this guide is written for developers who configured the proxy themselves in Playwright or Puppeteer, but every error string means exactly the same thing in the address bar.

## Two errors, two stages

For an HTTPS destination through an HTTP proxy, Chromium resolves the proxy hostname, opens a TCP connection to it, sends `CONNECT host:443`, waits for a `2xx` reply, and only then starts the destination TLS handshake inside the tunnel. The two error codes mark two different points on that path:

- **`ERR_PROXY_CONNECTION_FAILED`**: the proxy hostname did not resolve, nothing accepted the TCP connection, or the connection to the proxy failed before any HTTP was exchanged. A common self-inflicted variant is writing `https://` in front of a gateway that only speaks plain HTTP, which makes Chromium attempt a TLS handshake with a server that expects cleartext.
- **`ERR_TUNNEL_CONNECTION_FAILED`**: the TCP connection worked and the proxy answered the `CONNECT`, but not with a success status. Chromium discards the status code, the reason phrase and any body the proxy sent.

Everything else you may see (`ERR_CONNECTION_RESET`, `ERR_INVALID_HTTP_RESPONSE`, `ERR_SOCKS_CONNECTION_FAILED`, a plain navigation timeout) belongs to a different stage and is covered further down.

## What each proxy failure looks like

The table records what each client reported for one deliberate proxy behaviour. Playwright used its `proxy` launch option; Puppeteer used raw Chromium flags (`--proxy-server`) with `page.authenticate` for credentials; curl used `-x` with `--proxy-user`. All three talked to the same Chromium 153 build or curl 8.18 on Linux; details are in the method section.

| Proxy behaviour | Playwright 1.63 (Chromium 153) | Puppeteer 25 (same Chromium) | curl 8.18 |
| --- | --- | --- | --- |
| Proxy hostname does not resolve | `ERR_PROXY_CONNECTION_FAILED` | same | `(5) Could not resolve proxy` |
| Nothing listens on the proxy port | `ERR_PROXY_CONNECTION_FAILED` | same | `(7) Failed to connect` |
| `https://` scheme, but the proxy speaks plain HTTP | `ERR_PROXY_CONNECTION_FAILED` | same | `(35) TLS connect error` |
| Proxy accepts TCP and never answers | navigation timeout | navigation timeout | `(28) Connection timed out` |
| Proxy accepts TCP and closes at once | `ERR_CONNECTION_RESET` | same | `(56) Proxy CONNECT aborted` or `(56) Recv failure: Connection reset by peer` |
| Port answers, but not with HTTP | `ERR_INVALID_HTTP_RESPONSE` | same | `(56) Proxy CONNECT aborted` |
| CONNECT answered 403 | `ERR_TUNNEL_CONNECTION_FAILED` | same | `(7) CONNECT tunnel failed, response 403` |
| CONNECT answered 429 | `ERR_TUNNEL_CONNECTION_FAILED` | same | `(7) CONNECT tunnel failed, response 429` |
| CONNECT answered 502 or 503 | `ERR_TUNNEL_CONNECTION_FAILED` | same | `(7) CONNECT tunnel failed, response 502` or `503` |
| CONNECT answered 407, no credentials configured | navigation resolves with status 407, then `ERR_TUNNEL_CONNECTION_FAILED` on the request | `ERR_INVALID_AUTH_CREDENTIALS` | `(7) CONNECT tunnel failed, response 407` |
| CONNECT answered 407, wrong credentials | navigation resolves with status 407, then `ERR_TUNNEL_CONNECTION_FAILED` on the request | navigation resolves with status 407 and Chromium's error page, then `ERR_HTTP_RESPONSE_CODE_FAILURE` | `(7) CONNECT tunnel failed, response 407` |
| Correct credentials | 200 from the destination | 200 | 200, `http_connect` 200 |
| CONNECT answered 200, then the proxy drops the tunnel | `ERR_CONNECTION_CLOSED` or `ERR_CONNECTION_RESET` | `ERR_CONNECTION_RESET` | `(35) Send failure: Broken pipe` |
| `socks5://` scheme against an HTTP proxy | `ERR_SOCKS_CONNECTION_FAILED` | same | `(97) Received invalid version in initial SOCKS5 response` |
| `socks5://` with a username and password | refused at launch: `Browser does not support socks5 proxy authentication` | not exercised | not exercised |
| Plain `http://` destination, proxy rejects with 403 or 407 | no error: the proxy's own 403 or 407 page is the navigation response | same | 403 or 407 with `http_connect` 000 |

Three rows deserve a second look. Every non-success `CONNECT` status from 403 to 503 collapses into one browser error, so the browser alone cannot tell you whether you are blocked, throttled or looking at a broken upstream. A 407 is the only status Chromium handles specially, and Playwright and Puppeteer surface it differently. And a plain `http://` destination never produces a tunnel error, because there is no tunnel: the proxy's rejection arrives as an ordinary response with the proxy's status and body, which is why the same script can "work" on an HTTP test page and fail on the HTTPS site you care about.

## Recover the CONNECT status with curl

curl keeps the status that Chromium throws away. Run one bounded request against the same gateway, destination and credentials your browser script uses, and print both the destination status and the `CONNECT` status. The command reads `PROXY_URL`, `PROXY_USERNAME` and `PROXY_PASSWORD` from the environment so the password never appears in a shell argument or history entry; curl 8.3 or later is required for the variable expansion.

```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` with a destination status means the tunnel works and the browser problem is elsewhere. A non-zero `connect` value is the status Chromium hid:

- **407**: the proxy wants credentials it did not accept. Check the account or sub-user, percent-encoding, and whether this gateway authenticates by password or by allowed source IP. The [407 guide](https://ipvolt.com/guides/fix-proxy-error-407) walks through that order.
- **403**: the proxy accepted the connection but refuses this tunnel. Typical causes are a destination or port outside the provider's allowed list, an account restriction, or a country or session parameter the gateway rejects.
- **429**: concurrency or rate limits at the proxy. Look for a `Retry-After` header in `curl -v` output and reduce parallel browsers before retrying.
- **502, 503, 504**: the proxy could not reach or select an upstream exit. Retrying immediately rarely helps; note the time and ask the provider what their gateway logged.

`connect=000` with a curl exit code other than 0 means the failure happened before any `CONNECT` reply, which matches the `ERR_PROXY_CONNECTION_FAILED` and reset rows above. The [timeout guide](https://ipvolt.com/guides/proxy-timeout-troubleshooting) shows how to time each stage when the failure is slow rather than immediate.

## Playwright: a 407 does not throw

With no proxy credentials, or wrong ones, `page.goto` did not reject in the recorded run. It resolved with a response whose status was 407 and whose `statusText` was `Proxy Authentication Required`, with the proxy's `Proxy-Authenticate` header readable on that response and an empty body; a `requestfailed` event with `ERR_TUNNEL_CONNECTION_FAILED` fired for the same navigation. A script that only awaits `goto` and continues therefore keeps running against an empty page. Check `response.ok()` and treat a 407 as a proxy problem, not a destination problem:

```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();
}
```

Put proxy credentials in `proxy.username` and `proxy.password`. In the recorded run, credentials supplied through the context's `httpCredentials` option also satisfied the proxy challenge, because Playwright answers Chromium's authentication request from either source. Do not rely on that: `httpCredentials` is meant for the destination, and reusing it for the proxy sends a site login to the proxy operator.

Two more Playwright details from the run. Playwright adds `<-loopback>` to the bypass list itself, so a destination on `localhost` or `127.0.0.1` goes through the proxy; raw Chromium does the opposite (see the Puppeteer section). And a `socks5://` server with a username and password is refused before the browser starts, with `Browser does not support socks5 proxy authentication`. Chromium has no SOCKS5 authentication support, so the fix is an HTTP gateway with Basic authentication or IP allow-listing, not a different SOCKS setting. The [HTTP vs SOCKS5 guide](https://ipvolt.com/guides/http-vs-socks5-proxies) covers what changes between the two.

## Puppeteer: authenticate before the first navigation

Puppeteer passes the proxy to Chromium as a flag and answers 407 challenges only after `page.authenticate` has been called. Without it, the recorded run produced `net::ERR_INVALID_AUTH_CREDENTIALS`, a code many people never connect with a proxy. With wrong credentials, `goto` resolved with status 407 and Chromium's built-in error page as the body, plus a `requestfailed` event carrying `ERR_HTTP_RESPONSE_CODE_FAILURE`.

```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();
}
```

Two Chromium defaults matter here. First, Chromium bypasses the proxy for `localhost`, `127.0.0.1` and `[::1]` unless the bypass list contains `<-loopback>`. In the run, a Puppeteer script pointed at a proxy that rejected every tunnel still loaded a local HTTPS page with status 200, because the proxy was never consulted. A test that "passes" against a local server proves nothing about the proxy. Second, a raw Chromium launched with `--proxy-server` routed its own background traffic through the proxy as well: the recorded proxy log shows a `GET` to `clients2.google.com` and a `CONNECT` to `update.googleapis.com:443` before the page navigation. Expect those hostnames in provider logs and metered traffic, and do not mistake their failures for your navigation failing. Playwright's launch flags did not produce that traffic in the same run.

## Errors that are not tunnel errors

- **`ERR_CONNECTION_RESET` or `ERR_CONNECTION_CLOSED` right after connecting**: the proxy accepted the TCP connection and dropped it, either before answering `CONNECT` or immediately after a 200. Providers do this when an exit is unavailable or the connection is being rate limited at the socket level. curl reports `Proxy CONNECT aborted` or `Send failure: Broken pipe` for the same behaviour.
- **`ERR_INVALID_HTTP_RESPONSE`**: something answered on that port, but not in HTTP. Usually the port number belongs to a SOCKS listener, a TLS-only listener or a different service.
- **`ERR_SOCKS_CONNECTION_FAILED`**: you configured `socks5://` and the server spoke HTTP. Swap the scheme to match the gateway's documented protocol before changing anything else.
- **A navigation timeout with no net error**: the proxy accepted the connection and never replied. Raising the timeout will not help; verify the port and protocol with curl, which times out with `(28)` in the same situation.
- **`ERR_PROXY_CONNECTION_FAILED` on a gateway that works in curl**: compare the scheme. Chromium attempts TLS for `https://` proxies, and most gateways are plain `http://` endpoints that carry HTTPS destinations inside the tunnel.

## A diagnostic order

1. Reproduce with curl using the same gateway, destination and credentials, and read `http_connect`. This converts the browser's one-word error into the proxy's actual status in under a minute.
2. Confirm the proxy scheme (`http://`, `https://` or `socks5://`) against the provider's current instructions. Two of the three `ERR_PROXY_CONNECTION_FAILED` rows above are scheme mistakes.
3. Put credentials where the tool expects them: `proxy.username` and `proxy.password` in Playwright, `page.authenticate` before the first navigation in Puppeteer. Check `response.status() === 407` explicitly in both.
4. Test an HTTPS destination, not an HTTP one. HTTP destinations skip `CONNECT` and hide tunnel problems behind ordinary proxy responses.
5. Check bypass rules. Local and internal destinations bypass the proxy in raw Chromium, and corporate or environment proxy settings can override what you passed.
6. Report with the client versions, UTC time, gateway host and port, destination hostname and the curl `http_connect` value. Leave passwords, credential-bearing URLs and raw traces out; the [407 guide](https://ipvolt.com/guides/fix-proxy-error-407) has a sanitised report template.

## Method and downloads

ipvolt ran the reproduction on 17 September 2026 on a Linux host with Node.js 24.20.0, Playwright 1.63.0 (Chromium 153.0.8010.12, headless), puppeteer-core 25.11.0 driving the same Chromium build, and curl 8.18.0. Each "proxy" was a small Node.js server on 127.0.0.1 with exactly one behaviour; the destination was a local HTTPS server with a throwaway self-signed certificate, so the browser cases ignored destination certificate errors, which does not affect how Chromium talks to the proxy. Firefox and WebKit were not exercised, and no commercial gateway was involved: the table describes Chromium's reaction to a proxy behaviour, not any provider's policy.

The [lab archive](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/chromium-proxy-tunnel-errors.zip) contains the [lab script](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/tunnel-errors-lab.mjs), the [package manifest](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/package.json), the [README](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/README.md) and the [recorded results](https://ipvolt.com/downloads/chromium-proxy-tunnel-errors/results.json), including the proxy-side log that shows whether each client reached the proxy and whether credentials arrived. It runs on loopback only and needs no proxy account.

If you want to hear when ipvolt access opens, [join the early-access list](https://ipvolt.com/#waitlist-closing). One email when access opens. Nothing else. These examples are client diagnostics, not documentation of an available ipvolt endpoint.

## Sources & further reading

- [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)

## Related guides

- [Configure a proxy in Playwright](https://ipvolt.com/guides/playwright-proxy-setup.md)
- [Fix proxy error 407 without guessing](https://ipvolt.com/guides/fix-proxy-error-407.md)
- [Troubleshoot proxy timeouts one stage at a time](https://ipvolt.com/guides/proxy-timeout-troubleshooting.md)
- [HTTP vs SOCKS5 proxies: choose by the connection](https://ipvolt.com/guides/http-vs-socks5-proxies.md)

## About ipvolt

Examples use generic proxy settings, with links to the original technical documentation. Product-specific behavior must be checked with your provider. ipvolt is still in development.

## Know when access opens.

ipvolt · In development

We’re building proxy infrastructure for developers and data teams. Join the interest list for a heads-up when ipvolt is ready.

Consent: One email when access opens. Nothing else.

[Get early access](https://ipvolt.com/guides/fix-err-tunnel-connection-failed#waitlist-closing). Use the email form on this page to join the interest list.

[Privacy](https://ipvolt.com/privacy)

