The error arrives as a rejected promise from page.goto: Error: net::ERR_PROXY_CONNECTION_FAILED at https://example.com/. The net:: prefix marks it as one of Chromium's network errors, the same family a desktop Chrome shows on its grey error page, and this one is the same error a person sees when Chrome cannot reach a configured proxy. The difference in automation is that the proxy was configured by your code, the browser runs somewhere your code may not, and the frameworks each have their own rules about how a proxy and its credentials are passed. Those three facts account for almost every instance.
This guide covers the errors Chromium reports through Puppeteer, Playwright, and Selenium when a proxy is involved, why the same proxy that works in curl fails in the browser, and how each framework wants the proxy and the credentials supplied.
The errors and what each one means
Error on page.goto | What Chromium is saying | Usual cause in automation |
|---|---|---|
net::ERR_PROXY_CONNECTION_FAILED | Could not connect to the proxy at all | Wrong address or port; proxy down; unreachable from the container; unsupported scheme |
net::ERR_TUNNEL_CONNECTION_FAILED | Reached the proxy; it refused the HTTPS tunnel | Credentials not supplied; HTTP-only proxy; port or destination refused |
net::ERR_NO_SUPPORTED_PROXIES | None of the proxies in the setting is usable | A scheme Chromium does not support, or credentials inside the URL |
net::ERR_SOCKS_CONNECTION_FAILED | The SOCKS handshake failed | Authentication required, wrong protocol on that port, or a SOCKS reply |
net::ERR_INVALID_AUTH_CREDENTIALS | The proxy rejected the credentials | Wrong username or password in authenticate or the proxy option |
Timeout ... exceeded (framework timeout) | The proxy accepted and then stalled | Overloaded or dying proxy, or a target dropping the exit |
The first one is the subject of this page, but the others share its causes, and a job that produces one usually produces several.
Cause 1: the proxy is unreachable from where the browser runs
The proxy works in curl on your machine. The browser runs in a Docker container, a CI runner, a cloud function, or a remote grid. 127.0.0.1 in the container is the container. A proxy on the host, or a local forwarder on your laptop, is unreachable at that address from inside, and Chromium reports exactly this error.
Use the host's address as seen from the container (Docker Desktop provides host.docker.internal; on Linux, the host's bridge address or --network host), or run the forwarder inside the same container or network. The same applies to a proxy on a private network the runner cannot reach, and to a proxy whose port the runner's network blocks. Test from inside the environment, not from your desk: docker exec -it <container> curl -v -x http://proxy:port https://example.com/ tells you what the browser will see.
Cause 2: the address, the port, or the proxy itself
The boring causes are still the common ones. A typo in the host, a port for the wrong protocol, a rotating gateway's hostname that resolves only on some networks, or a proxy that has died. Free proxies die within hours and are shared by thousands of processes, and a scraper on a public list spends most of its time in this error. Run the address through our proxy checker; a dead result ends the investigation, and a live result narrows it to the scheme and reachability causes.
Cause 3: the scheme and the credentials
Chromium's --proxy-server accepts http://, https://, socks4://, and socks5:// addresses, and it does not accept credentials inside the URL. socks5h://, which curl and Python understand, is not a Chromium scheme. A URL with user:pass@ in it is ignored or rejected depending on version, and either way the proxy answers with an authentication challenge the browser cannot satisfy, which surfaces as ERR_TUNNEL_CONNECTION_FAILED, ERR_NO_SUPPORTED_PROXIES, or this error. Credentials go through the framework, not the URL:
Puppeteer. Launch with the proxy and authenticate the page before navigating:
const browser = await puppeteer.launch({
args: ["--proxy-server=http://gateway.example:8080"],
});
const page = await browser.newPage();
await page.authenticate({ username: "user", password: "pass" });
await page.goto("https://example.com/");
Playwright. The proxy option carries credentials and works at launch or per context, which is how one browser can use different exits for different contexts:
const browser = await chromium.launch({
proxy: { server: "http://gateway.example:8080", username: "user", password: "pass" },
});
const context = await browser.newContext({
proxy: { server: "http://gateway.example:8080", username: "user-session-abc123", password: "pass" },
});
Selenium. The driver passes --proxy-server through the browser options, and there is no equivalent of authenticate: Chromium raises an authentication dialog that Selenium cannot answer. The working routes are IP authentication with the provider, so no credentials are needed; a small extension packaged with the profile that answers the proxy challenge; or a local forwarding proxy that adds the credentials and exposes an unauthenticated proxy on localhost. Our Selenium proxy guide shows the extension approach.
Cause 4: SOCKS5 with authentication
Chromium does not do SOCKS5 authentication. A socks5:// proxy that requires a username and password is unusable from Chromium directly, in every framework, and produces ERR_SOCKS_CONNECTION_FAILED or this error. Use the provider's HTTP port with credentials, which nearly every provider offers alongside SOCKS, or IP authentication on the SOCKS port, or a local forwarder that authenticates to the SOCKS proxy and exposes it plainly on localhost. SOCKS5 proxy errors covers what the handshake failures mean when the proxy is reached.
Cause 5: an HTTP-only proxy and an HTTPS target
Free lists are full of proxies that fetch plain http:// pages and refuse the CONNECT request every https:// page needs. In the browser that is ERR_TUNNEL_CONNECTION_FAILED on the first HTTPS navigation, and since almost every target is HTTPS, such a proxy is useless for automation. The checker reports HTTPS support separately from HTTP for exactly this reason, and our guide to ERR_TUNNEL_CONNECTION_FAILED shows how to read the proxy's CONNECT answer with curl.
Cause 6: the environment variable that does nothing
Scripts often set HTTP_PROXY in the environment and expect the browser to follow it, because curl, pip, and Node's fetch helpers do. The bundled Chromium that Puppeteer and Playwright download does not reliably pick up shell proxy variables, and its behaviour differs by platform and flags. Pass the proxy explicitly, every time, and pass the bypass list explicitly too: --proxy-bypass-list="localhost;127.0.0.1;*.internal" in Puppeteer's args, or the bypass field of Playwright's proxy option, so that health checks and local services do not go through the exit.
Cause 7: the proxy accepted, then stalled
Not this error by name, but the same job: the framework's navigation timeout fires because the proxy accepted the connection and never delivered. Overloaded free proxies do this, and so does a target that drops the exit's address at the network level. Raise the timeout only after confirming the proxy answers the checker in milliseconds; a proxy that takes seconds there will time out under a browser's page load. A target that drops one exit and not another is the address problem covered in how websites detect proxies.
A launch that avoids most of it
- Test the proxy from inside the environment the browser runs in, with curl, before the first launch.
- Pass the proxy explicitly with a supported scheme and no credentials in the URL.
- Supply credentials through
page.authenticate, Playwright'sproxyoption, or IP authentication for Selenium. - Use the HTTP port for authenticated access; keep SOCKS for IP-authenticated setups.
- Set the bypass list so local traffic stays local.
- Prefer a per-context proxy in Playwright when different contexts need different exits, and keep one exit per context for the life of a session.
The frameworks' own guides cover the rest of the setup: Puppeteer, Playwright, and Selenium. For the exits themselves, a residential gateway with session control in the username gives each context a stable address the target treats as a household, on an HTTP port with credentials the frameworks can send, which removes causes 2 through 5 in one step.
Where curl and the browser differ
net::ERR_PROXY_CONNECTION_FAILED means the browser never reached the proxy. Check the address from inside the environment the browser runs in, especially inside Docker. Use a scheme Chromium supports, keep credentials out of the URL and pass them through the framework, avoid authenticated SOCKS5, and use a proxy that carries HTTPS. If curl from the same place succeeds and the browser still fails, the scheme or the credentials are the difference.