๐ŸŽ‰ Premium Proxies ยท 24-Hour Free TrialClaim Now
Guides

Selenium Chrome Proxy Login Popup: How to Fix It

Fix the Selenium proxy authentication popup and ERR_NO_SUPPORTED_PROXIES: why Chrome drops user:pass, plus three tested fixes with working Python code.

S SparkProxy 6 8 min read
Share

The selenium proxy authentication popup shows up when you hand Chrome a proxy that wants a username and password, and Chrome has no way to answer. If you tried the obvious move, --proxy-server=http://user:pass@host:port, you either got that popup or a page reading ERR_NO_SUPPORTED_PROXIES, and in both cases driver.get() returned as if nothing went wrong. This guide shows why Chrome behaves this way, how to confirm which failure you have in under a minute, and the three fixes we tested on Chrome 156 and Selenium 4.52.0 on 2026-10-30.

The Two Symptoms and What Each One Means

We reproduced both symptoms with a local test proxy that demands HTTP Basic auth exactly the way an authenticated gateway does, then logged every request that reached it. Test bench: Windows Server 2022, Chrome 156.0.8078.74, Selenium 4.52.0 (released on PyPI on 2026-10-29), Python 3.14.2.

Symptom A: credentials inside --proxy-server. Every variant we tried produced Chrome's network error page with the code ERR_NO_SUPPORTED_PROXIES, and the proxy logged zero requests. Chrome never even dialled it.

`--proxy-server` valueWhat the page showedRequests that reached the proxy
`http://user:pass@127.0.0.1:18899``ERR_NO_SUPPORTED_PROXIES`0
`user:pass@127.0.0.1:18899``ERR_NO_SUPPORTED_PROXIES`0
`https://user:pass@127.0.0.1:18899``ERR_NO_SUPPORTED_PROXIES`0
`socks5://user:pass@127.0.0.1:18899``ERR_NO_SUPPORTED_PROXIES`0
`http://127.0.0.1:18899` (no credentials)Empty page3, all without `Proxy-Authorization`

Chromium's own error list defines it as NET_ERROR(NO_SUPPORTED_PROXIES, -336), "There are no supported proxies in the provided list." Our reading: the entry with credentials doesn't parse as a proxy, gets dropped, and the list Chrome is left with is empty.

Symptom B: the Sign in dialog. Drop the credentials and Chrome does reach the proxy, which answers 407 Proxy Authentication Required. In a headed browser Chrome then asks a human. The dialog's text comes from Chromium's string table (components/login_dialog_strings.grdp): the title is "Sign in" and the body reads "The proxy %s requires a username and password." That dialog is browser UI, not page DOM, so find_element can't reach it and send_keys has nothing to type into.

Headless is quieter and worse. With --headless=new there's no dialog at all. driver.get() returned in 0.1 seconds without raising, and driver.page_source was literally . A scraper that only catches exceptions will record that as a successful, empty page.

Why Chrome Throws Away the user:pass Part

This is documented behaviour rather than a Selenium bug. Chromium's proxy design notes (net/docs/proxy.md) say, in the section on credentials in manual proxy settings, that Chrome "does not implement this, and will not use any credentials embedded in the proxy settings. Proxy authentication will instead go through the ordinary flow to find credentials."

The "ordinary flow" is the 407 challenge and response. Chrome connects with no credentials, the proxy refuses with a Proxy-Authenticate header, and Chrome asks whatever is wired up to answer: its password store, an extension, a DevTools client, or a person at the keyboard. Selenium's ChromeOptions has no slot for proxy credentials because Chrome has nowhere to receive them on the command line. If the 407 handshake itself is new to you, the proxy authentication explainer covers it.

SOCKS5 is a harder wall. The same document states that "No authentication methods are supported for SOCKSv5 in Chrome." There is no challenge to answer at all, so the CDP handler below can't help with an authenticated SOCKS5 upstream. Whitelisting can, and so can anything that performs the SOCKS5 login outside Chrome. That's why the socks5://user:pass@ row above fails identically.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Confirm the Cause in Under a Minute

Run these in order. Each costs less than the next.

1. Read your own launch arguments. If the --proxy-server string contains @, you have Symptom A and can skip to the fixes. Nothing else needs testing.

2. Make the silent failure loud. Chrome's error page carries the code in an element with the class error-code, so you can check it right after driver.get():

def proxy_failure(driver):
    """Return Chrome's net error code, 'EMPTY_PAGE', or None if the page loaded."""
    code = driver.execute_script(
        "const e = document.querySelector('#main-frame-error .error-code');"
        "return e ? e.textContent.trim() : null;")
    if code:
        return code  # e.g. ERR_NO_SUPPORTED_PROXIES, ERR_TUNNEL_CONNECTION_FAILED
    if driver.page_source.strip() == "<html><head></head><body></body></html>":
        return "EMPTY_PAGE"  # what an unanswered 407 looked like in headless Chrome 156
    return None

3. Prove the credentials outside the browser. If curl can't get through, no Chrome fix will help:

export SP_USER='your_username'
export SP_PASS='your_password'
curl -s -o /dev/null -w "tunnel=%{http_connect} page=%{http_code}\n" \
  -x http://gateway.sparkproxy.io:11000 -U "$SP_USER:$SP_PASS" \
  https://www.sparkproxy.io/

tunnel=200 means the gateway accepted you and the problem is purely how Chrome receives the credentials. tunnel=407 means the credentials themselves are rejected. Fix that first, because every option below will just forward the same bad password.

Choosing Between the Three Fixes

All three move authentication out of Chrome's command line. They differ in where the credentials live and what breaks when your setup changes.

Fix 1: IP whitelistFix 2a: selenium-wireFix 2b: CDP FetchFix 3: local forwarder
Credentials in your scriptNoneYesYesNo (in the forwarder)
Extra packagesNoneselenium-wire plus 3 pinsNone beyond SeleniumNone (or mitmproxy)
Decrypts your HTTPSNoYesNoNo
Covers new tabs and windowsYesYesOnly the tab you attached toYes
Works for authenticated SOCKS5YesDocumented, not tested by usNoOnly with a SOCKS-capable forwarder
Breaks whenEgress IP changesA dependency moves againCode opens a new tabThe forwarder process dies
Project status (read 2026-10-30)n/aArchived, last release 2022Standard Chrome protocolYour code, or mitmproxy 12.3.1

Short version: on a server with a fixed public IP, whitelist and stop there. On laptops, CI runners or anywhere the IP moves, use the local forwarder. Keep CDP for when you can't run a second process, and selenium-wire for when you also need its request inspection.

Fix 1: Authenticate the Machine, Not the Browser

With IP whitelisting, the gateway decides by the source address of the connection, so it never sends a 407 and Chrome never has to answer one. SparkProxy supports both IP whitelist and user:pass authentication on the same plans. Add the public IP of the machine that runs Chrome in the dashboard, then launch with a bare host and port:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--headless=new")
options.add_argument("--proxy-server=http://gateway.sparkproxy.io:11000")  # no user:pass
driver = webdriver.Chrome(options=options)
driver.get("https://www.sparkproxy.io/")
print(driver.title)
driver.quit()

Port 11000 hands out a new IP per connection (Random Rotation). If the job needs one exit IP across a login flow, use the Sticky Sessions port, 11002, with the same launch line.

The catch is that "the machine" means the address the gateway sees, which isn't always the one you think. Check it from the same host, without the proxy:

curl -s https://checkip.amazonaws.com

Selenium setups get this wrong in a few predictable ways. On a Grid, Chrome runs on the node, so the node's address is the one to whitelist, not the address of the box running pytest. Docker containers often egress through a NAT gateway. Cloud instances can come back from a reboot on a different address, and a host that prefers IPv6 will present an address your IPv4 entry doesn't match. For when whitelisting is the wrong choice altogether, read what IP whitelisting is and when it fits.

Fix 2a: selenium-wire, and the Three Pins It Needs Now

selenium-wire runs its own proxy inside your Python process, points Chrome at it, and sends the upstream credentials itself. The basic configuration is in the Selenium proxy setup guide, so here is what that guide can't tell you: the state of the project today.

On 2026-10-30 the GitHub repository wkeeling/selenium-wire was archived, its last commit was dated 2024-01-03, and the newest PyPI release was 5.1.0 from 2022-10-15. The README opens with "Selenium Wire is no longer being maintained."

So we installed it fresh on Python 3.14.2 next to Selenium 4.52.0 and wrote down each failure:

StepErrorCausePin
`from seleniumwire import webdriver``ModuleNotFoundError: No module named 'pkg_resources'`New venvs ship no setuptools, and setuptools 82.0.0 removed `pkg_resources``setuptools<82`
Same import, again`ModuleNotFoundError: No module named 'blinker._saferef'`pip resolved blinker 1.9.0`blinker==1.7.0`
First HTTPS `driver.get()``net::ERR_CONNECTION_CLOSED`, log shows `AttributeError: 'X509' object has no attribute 'get_extension'`pyOpenSSL 26.2.0 (2026-05-04) removed deprecated X509 extension APIs`pyOpenSSL<26.2`

With all three pins, the page loaded through the authenticated proxy:

pip install selenium-wire "setuptools<82" "blinker==1.7.0" "pyOpenSSL<26.2"
import os
from selenium.webdriver.chrome.options import Options
from seleniumwire import webdriver

upstream = "http://{}:{}@gateway.sparkproxy.io:11000".format(
    os.environ["SP_USER"], os.environ["SP_PASS"])
options = Options()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options, seleniumwire_options={
    "proxy": {"http": upstream, "https": upstream, "no_proxy": "localhost,127.0.0.1"}})
driver.get("https://www.sparkproxy.io/")
print(driver.title)
driver.quit()

Two costs come with it. The traceback above runs through seleniumwire/thirdparty/mitmproxy: selenium-wire decrypts and re-encrypts every HTTPS connection with a bundled, years-old copy of mitmproxy, so the TLS handshake the target sees comes from that stack rather than from Chrome. And each future Python or OpenSSL release can add a fourth pin. If you need its driver.requests inspection, it still works. If you only wanted authentication, the next two fixes carry less baggage.

Fix 2b: Answer the 407 Yourself With CDP Fetch.authRequired

Chrome's DevTools Protocol can answer the challenge for you. Per the protocol definition, Fetch.enable with handleAuthRequests: true makes Chrome pause on every 401 or 407 and emit Fetch.authRequired. The event's authChallenge.source is either "Server" or "Proxy", and you reply with Fetch.continueWithAuth set to ProvideCredentials, CancelAuth or Default.

Selenium's execute_cdp_cmd can send commands but can't receive events, so the function below opens its own DevTools socket using websocket-client, which Selenium already installs as a dependency:

import json
import os
import threading
import urllib.request

import websocket  # websocket-client, installed with Selenium
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


def answer_proxy_auth(driver, username, password):
    """Answer 407 challenges for the driver's current tab over CDP (Fetch domain)."""
    host = driver.capabilities["goog:chromeOptions"]["debuggerAddress"]
    with urllib.request.urlopen(f"http://{host}/json") as resp:
        targets = json.load(resp)
    page = next(t for t in targets if t["type"] == "page")
    ws = websocket.create_connection(page["webSocketDebuggerUrl"], suppress_origin=True)
    ws.send(json.dumps({"id": 1, "method": "Fetch.enable", "params": {
        "handleAuthRequests": True, "patterns": [{"urlPattern": "*"}]}}))
    msg_id = 1
    tried = set()  # request ids we already answered once

    def loop():
        nonlocal msg_id
        while True:
            try:
                msg = json.loads(ws.recv())
            except (websocket.WebSocketException, OSError):
                return  # browser closed
            method, params = msg.get("method"), msg.get("params", {})
            if method == "Fetch.requestPaused":
                reply = ("Fetch.continueRequest", {"requestId": params["requestId"]})
            elif method == "Fetch.authRequired":
                rid = params["requestId"]
                if params["authChallenge"].get("source") != "Proxy":
                    answer = {"response": "Default"}  # a site login, not ours to answer
                elif rid in tried:
                    answer = {"response": "CancelAuth"}  # wrong password: stop the retry loop
                else:
                    tried.add(rid)
                    answer = {"response": "ProvideCredentials",
                              "username": username, "password": password}
                reply = ("Fetch.continueWithAuth",
                         {"requestId": rid, "authChallengeResponse": answer})
            else:
                continue
            msg_id += 1
            ws.send(json.dumps({"id": msg_id, "method": reply[0], "params": reply[1]}))

    threading.Thread(target=loop, daemon=True).start()
    return ws


options = Options()
options.add_argument("--headless=new")
options.add_argument("--proxy-server=http://gateway.sparkproxy.io:11000")  # no credentials here
driver = webdriver.Chrome(options=options)
answer_proxy_auth(driver, os.environ["SP_USER"], os.environ["SP_PASS"])
driver.get("https://www.sparkproxy.io/")
print(driver.title)
driver.quit()

The tried set is there because of something we measured. Our first version answered every challenge, and with a wrong password a single driver.get() sent the bad credentials 31 times before Chrome gave up. With the guard it sent them once and stopped.

Know the limits before you ship it. The handler covers the one tab it attached to, so a window.open() or a new window arrives without it. It needs debuggerAddress, which means Chrome and your script on the same host. On a remote Grid this route gets awkward fast.

Selenium 4.52.0 also ships a WebDriver BiDi helper, driver.network.add_auth_handler(username, password), built on the standard network.authRequired event. In our run it didn't get past the first proxy challenge: the page load timed out after 20 seconds and the callback thread logged WebSocket connection closed while waiting for response to BiDi command 3. That's one test on one version, so recheck it on later releases, but don't build on it today without testing.

Fix 3: Put a Credential-Adding Proxy on localhost

Run a tiny proxy on 127.0.0.1 that needs no auth from Chrome and adds Proxy-Authorization on the way out. Chrome talks to localhost with a bare --proxy-server, never meets a 407, and never holds the password. This one script handles HTTPS tunnels (CONNECT) and plain HTTP:

# auth_forwarder.py: listen on 127.0.0.1:8888 with no auth, forward to the
# upstream proxy and add Proxy-Authorization to every request it relays.
import asyncio
import base64
import os

LISTEN_PORT = 8888
UP_HOST, UP_PORT = os.environ.get("SP_GATEWAY", "gateway.sparkproxy.io:11000").rsplit(":", 1)
TOKEN = base64.b64encode(f"{os.environ['SP_USER']}:{os.environ['SP_PASS']}".encode()).decode()


async def pipe(reader, writer):
    try:
        while data := await reader.read(65536):
            writer.write(data)
            await writer.drain()
    except (ConnectionError, asyncio.IncompleteReadError):
        pass
    finally:
        writer.close()


async def handle(client_r, client_w):
    try:
        head = await client_r.readuntil(b"\r\n\r\n")
    except (asyncio.IncompleteReadError, asyncio.LimitOverrunError):
        client_w.close()
        return
    lines = head.decode("latin-1").split("\r\n")
    # Drop anything the browser sent about the proxy, then add our own credentials.
    # Connection: close stops the browser reusing this socket for a second plain-HTTP
    # request that would reach the upstream without the header.
    kept = [l for l in lines[1:] if l and not l.lower().startswith(("proxy-", "connection:"))]
    kept += [f"Proxy-Authorization: Basic {TOKEN}", "Connection: close"]
    up_r, up_w = await asyncio.open_connection(UP_HOST, int(UP_PORT))
    up_w.write("\r\n".join([lines[0], *kept, "", ""]).encode("latin-1"))
    await up_w.drain()
    await asyncio.gather(pipe(client_r, up_w), pipe(up_r, client_w))


async def main():
    server = await asyncio.start_server(handle, "127.0.0.1", LISTEN_PORT)
    print(f"forwarding 127.0.0.1:{LISTEN_PORT} -> {UP_HOST}:{UP_PORT}")
    async with server:
        await server.serve_forever()


if __name__ == "__main__":
    asyncio.run(main())

Start it, then point Chrome at it:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--headless=new")
options.add_argument("--proxy-server=http://127.0.0.1:8888")
driver = webdriver.Chrome(options=options)
driver.get("https://www.sparkproxy.io/")
print(driver.title)
driver.quit()

In our test, every one of the 21 requests Chrome made for that page reached the upstream with valid credentials. Because it binds only to 127.0.0.1, nothing else on the network can borrow your account through it.

If you'd rather not maintain code, mitmproxy (12.3.1 on PyPI, released 2026-10-25) does the same job in upstream mode. Its documented upstream_auth option "Add[s] HTTP Basic authentication to upstream proxy and reverse proxy requests", and ignore_hosts='.*' passes TLS through untouched, so there's no certificate to install:

mitmdump --mode upstream:http://gateway.sparkproxy.io:11000 \
  --set upstream_auth="$SP_USER:$SP_PASS" \
  --set ignore_hosts='.*' --listen-host 127.0.0.1 -p 8888

We ran that command against our test proxy and Chrome loaded the page through it on the first try. The forwarder pattern is the most portable of the three: it covers every tab, service workers included, it doesn't care which Selenium version you run, and on a Grid you start one per node.

What No Proxy Fix Will Solve

Once the page loads through the proxy, authentication is done. Anything that goes wrong afterwards belongs to a different layer, and changing how you authenticate won't touch it:

  • A site login. A 401 or a login form comes from the website. The CDP handler above deliberately answers "Server" challenges with Default for that reason.
  • ERR_PROXY_CONNECTION_FAILED or ERR_TUNNEL_CONNECTION_FAILED. Chromium describes the first as failing to resolve or connect to the proxy itself. That's host, port or firewall, before any credentials are exchanged.
  • Blocks, CAPTCHAs and fingerprinting. A clean IP doesn't hide navigator.webdriver or a headless fingerprint. For the browser side of a Selenium setup, see the datacenter proxies with Selenium WebDriver tutorial and the Chrome and Firefox proxy settings guide.

And if Chrome is only there to fetch rendered HTML, you can skip proxy authentication in the browser altogether. The SparkProxy Scraping API authenticates with an X-API-Key header and renders JavaScript by default (render_js defaults to true, 5 credits per request; render_js=false is a 1-credit plain fetch):

import os
import requests

r = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": os.environ["SPARK_API_KEY"]},
    params={"url": "https://www.sparkproxy.io/", "proxy_type": "premium"},
    timeout=90,
)
print(r.status_code, len(r.text))

New accounts get 1,000 free credits. For the proxies themselves, SparkProxy plans start from $75/month (USD, as listed on sparkproxy.io on 2026-10-09). They're sold as packs with unlimited bandwidth rather than per GB, and there's a 24-hour free trial with no credit card.

Frequently asked questions

FAQ

Chrome connected to your proxy without credentials and the proxy answered 407. Chrome ignores any username and password in --proxy-server, so it falls back to asking a person through its "Sign in" dialog. WebDriver can't type into that dialog because it's browser UI, not part of the page.

Chromium defines it as "There are no supported proxies in the provided list." In our tests on Chrome 156, any --proxy-server value containing user:pass@ produced it, and the proxy received no requests at all. Remove the credentials and authenticate with one of the three fixes instead.

No. Chromium's proxy documentation says Chrome "will not use any credentials embedded in the proxy settings." Use IP whitelisting, an auth handler (selenium-wire or CDP), or a local forwarder that adds the credentials outside Chrome.

No. The repository was archived, its last commit is dated 2024-01-03, and the latest release is 5.1.0 from 2022-10-15. It still works on Python 3.14 if you pin setuptools<82, blinker==1.7.0 and pyOpenSSL<26.2.

No. Headless Chrome shows nothing. In our test driver.get() returned without an exception and the page was an empty HTML document, so check for Chrome's error code or an empty page after every load rather than relying on exceptions.

Chrome supports no SOCKS5 authentication methods, so CDP can't help here. Whitelist your IP and use socks5://host:port. The other route is selenium-wire, whose README documents socks5://user:pass@ upstreams (we didn't test that path), or a SOCKS-capable local forwarder; the HTTP forwarder in this guide won't do it.

Special Discount ยท 20% off

Get 20% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Save up to 15% more on quarterly, half-yearly and yearly plans

Claim Discount

About the Author

The SparkProxy Technical Team builds and runs SparkProxy's proxy network and the SparkProxy Scraping API, and supports the developers who route Selenium, Playwright and plain HTTP traffic through them. Plans cover 1M+ premium IPs in 80+ countries over HTTP, HTTPS and SOCKS5, with IP whitelist and user:pass authentication. Every error string and request count in this guide came from our own test runs on 2026-10-30. Version and maintenance facts were read from Chromium source, the Chrome DevTools Protocol definition, PyPI and GitHub on that date.

Keep reading

Related articles