# ERR_TUNNEL_CONNECTION_FAILED in Chrome behind a proxy

> Chrome's tunnel connection failed error means the proxy refused the CONNECT tunnel. In our runs the usual cause was a sticky session the gateway had retired.

Source: https://mosaicbrowser.com/blog/err-tunnel-connection-failed

## What ERR_TUNNEL_CONNECTION_FAILED is telling you

Chrome's tunnel connection failed error means the proxy refused the CONNECT tunnel. In our runs the usual cause was a sticky session the gateway had retired.

![-111: The proxy said no, and Chrome threw away the reason](https://mosaicbrowser.com/covers/err-tunnel-connection-failed.webp)

ERR_TUNNEL_CONNECTION_FAILED means Chrome asked the proxy to open a tunnel and the proxy said no. To load `https://example.com` through an HTTP proxy, Chrome first sends `CONNECT example.com:443 HTTP/1.1` to the proxy and needs `200` back before a single byte of TLS goes out. A 403 from a blocklist, a 502 from a gateway whose upstream fell over and a 504 from an exit that never answered all end on the same grey page. None of them is about the site. In our own runs through residential gateways, the one we met most was a 504 from a sticky session the gateway had already retired.

## Where ERR_TUNNEL_CONNECTION_FAILED comes from in Chrome

Chromium numbers its network errors in one file, `net/base/net_error_list.h`, and this one is -111: "A tunnel connection through the proxy could not be established". The decision is made in `HttpProxyClientSocket`, in `http_proxy_client_socket.cc`, after it reads the proxy's reply headers. It switches on the status code. `200` opens the tunnel and `407` sends Chrome off to find proxy credentials. The `default:` branch, which is every other code from 301 to 504, returns -111. Two odd cases land there too: a `200` followed by bytes the proxy should not have sent yet, and a reply older than `HTTP/1.0`.

Three neighbours in the same file look similar and point at different things:

| Error | What failed |
| --- | --- |
| `ERR_PROXY_CONNECTION_FAILED` (-130) | Reaching the proxy at all: its name did not resolve, or the socket to it would not open |
| `ERR_TUNNEL_CONNECTION_FAILED` (-111) | The proxy answered, but the CONNECT came back as something other than 200 |
| `ERR_PROXY_AUTH_UNSUPPORTED` (-115) | The proxy asked for a login by a method Chrome does not speak |
| `ERR_SOCKS_CONNECTION_FAILED` (-120) | The SOCKS case: the connection through a SOCKS proxy to the target host failed |

A -111 therefore tells you more than it seems to: the proxy resolved, accepted the socket and wrote an HTTP reply. What it wrote is the part you cannot see. If you are not sure the port speaks HTTP at all, [the port number names the protocol](https://mosaicbrowser.com/blog/port-number-names-protocol) shows how we asked one seller's ports which protocol each spoke.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Provider minting](https://mosaicbrowser.com/features/proxy-providers)

## Why Chrome hides the proxy's reason

Gateways usually explain a refusal. A 504 arrives with a page saying the upstream timed out, and a 403 often names the rule that matched. Chrome reads that page's headers and throws the body away, on purpose.

The `default:` branch carries the reason in a comment that points at crbug 137891: the body is ignored "to avoid letting the proxy impersonate the target server". The address bar would show your bank's name over a page the proxy wrote. The next lines of that comment concede what it costs. Chromium's developers had seen 403, 404 and 501 bodies with a useful message in them, and Squid, for one, reports a failed DNS lookup as a 404 reading "The domain name does not exist." That message dies at this line.

curl has no such rule, so put the same proxy in front of curl and read the reply yourself:

```
curl -v -x http://user:pass@gate.example:8000 https://example.com/ -o /dev/null

```

In the `-v` output the CONNECT reply is the `< HTTP/1.1` line that comes before any TLS, for example `< HTTP/1.1 504 Gateway Timeout`, followed by whatever headers the gateway added. Inside Chrome, a capture from `chrome://net-export` holds the same headers under the event `HTTP_TRANSACTION_READ_TUNNEL_RESPONSE_HEADERS`, which is the browser's own record of what it received.

## The cause we kept hitting: a session the gateway had retired

Most fix-it pages for this error tell you to switch off the system proxy, which helps only if you never meant to have one. With a residential gateway and sticky sessions, look at the username instead.

Ours carried a token like `-session-<hex>-lifetime-60`, which asks the gateway to keep one exit for sixty minutes. After the sixty minutes the gateway stops honouring it, and nothing about the failure says "expired". During one measurement run the browser logged `net::ERR_TUNNEL_CONNECTION_FAILED` and `net::ERR_ABORTED`, and four sites in a row came back unreachable. We read that as four third-party sites having a bad afternoon. Our proxy checker, run a minute later, called the proxy healthy, with an exit in Walnut Ridge, Arkansas. An hour earlier the same profile had been coming out in Mililani Town, Hawaii. The session had aged out, and in between the gateway had been refusing the stale token.

The other shape is a dead exit. A residential pool hands out the occasional session that reaches nothing. When we went looking, we minted forty fresh sessions at one seller and the twenty-ninth was bad: it answered 504 four times in a row, on the original attempt and on three retries. The next fresh session we tried also answered 504, and the two after it answered 200. A 504 there is a property of the session, not of the gateway.

Both cases taught us something about reading the error. The four unreachable sites in the Arkansas run looked like four outages, and the cheaper explanation was one proxy, so now a cluster of failures through one exit sends us to the proxy first. That run also lasted longer than the session's sixty minutes, which made it two experiments on two addresses; a long job wants a session lifetime longer than the job, since every site you are signed in to sees the address change ([how accounts get linked](https://mosaicbrowser.com/guides/how-to-manage-multiple-accounts-without-getting-linked) is about exactly that). And the 504 run showed what a retry is worth on a sticky session: session 889053 failed four times because each retry asked the same dead exit. The fix is a new session id.

## How Mosaic handles a refused tunnel

Mosaic runs the same CONNECT itself before every launch, and a profile does not open behind a proxy that has not answered and tunnelled. Because the check is our code and not Chrome's, it keeps the status line Chrome would discard, so the refusal reads "Proxy returned HTTP 504" instead of a -111 in an empty tab.

What happens next depends on who made the session id. Save a seller's gateway line once as a provider, and each profile mints its own session from it; a dead one of those is swapped for a fresh session with the same pick, so a profile set to Germany still comes out in Germany, and the working token is written back so the next launch does not start on the dead one. A session id you typed yourself is never swapped. That launch is refused, because a profile that quietly came out somewhere else would be worse than one that did not open. [A session token on a rotating port](https://mosaicbrowser.com/blog/session-token-on-rotating-port) covers how the port and the username decide whether a session sticks in the first place.

## Questions

### What does ERR_TUNNEL_CONNECTION_FAILED mean?

Chrome asked the proxy to open a tunnel to the site with an HTTP CONNECT request, and the proxy answered with anything other than 200, or with a reply Chrome could not accept. It is net error -111 in Chromium's list.

### Why does Chrome not show the proxy's error message?

On purpose. Chromium ignores the body of a failed CONNECT reply so that a proxy cannot show you a page pretending to be the site you asked for. The status code and the reason are dropped with it.

### How do I find out why the tunnel failed?

Repeat the request outside the browser with curl -v and the same proxy. The CONNECT reply's status line, such as 403, 502 or 504, is printed in full. Chrome's net-export log records the same reply headers.

### Can a rotating or sticky proxy cause ERR_TUNNEL_CONNECTION_FAILED?

Yes, and it is the cause we hit most. A sticky session that has outlived its lifetime, or an exit that went dead, is refused by the gateway while a fresh session on the same gateway works.

## Related

[A session token on a rotating port is a 400, not a no-op](https://mosaicbrowser.com/blog/session-token-on-rotating-port): "Can not use -session- on rotating ports" means the port, not the username, decides stickiness. We had written the opposite, until we asked the gateway.

[The port number told us whether a proxy was SOCKS5](https://mosaicbrowser.com/blog/port-number-names-protocol): Nothing in host:port:user:pass says HTTP or SOCKS5, and curl assumes HTTP. At CatProxies we asked the port: 11000 answered SOCKS5, 10000 HTTP.

[How to manage multiple accounts without getting them linked](https://mosaicbrowser.com/guides/how-to-manage-multiple-accounts-without-getting-linked): Accounts get linked by shared cookies, IPs, fingerprints and habits. Give each its own profile, exit and history, then test it before you trust it.

[Introducing Mosaic, an anti-detect browser that lives on your machine](https://mosaicbrowser.com/blog/introducing-mosaic): Mosaic runs its own Chromium, draws each profile from a real machine and matches it to the proxy exit. Profiles stay on disk. One plan, no per-profile fees.

[A CONNECT to a raw IP means your machine resolved the name](https://mosaicbrowser.com/blog/a-numeric-connect-means-dns-leaked): To check if DNS goes through your proxy, read what the proxy is asked for. CONNECT example.com:443 means it resolved the name; a raw IP means you did.

[DNS prefetching, the lookup that skips your proxy](https://mosaicbrowser.com/blog/dns-prefetch-and-proxies): Chrome's DNS prefetcher resolves names through your own resolver even with a proxy set. Mapping every name to NOTFOUND makes the leak fail closed.

[A proxy that answers HTTP and refuses CONNECT](https://mosaicbrowser.com/blog/proxy-http-works-https-fails): A proxy that works for HTTP but not HTTPS is refusing CONNECT, the tunnel every HTTPS page needs. A check that fetches a plain-HTTP page cannot see it.

[socks5 and socks5h: where Chrome resolves the name](https://mosaicbrowser.com/blog/socks5-and-socks5h-in-chrome): In curl, socks5h sends the hostname to the proxy and socks5 resolves it locally. Chrome's socks5 already sends the name; the leak is elsewhere.

## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
