Zendriver Proxy With Authentication: Setup and Workarounds

Zendriver is the community-maintained fork of nodriver — faster to fix, but with the same authenticated-proxy gap. Here is the full proxy setup, what actually differs, and the workarounds that get user:pass working.

A zendriver proxy is set up exactly like a nodriver one — which is both the good news and the catch. zendriver (the cdpdriver/zendriver project) is the community-maintained fork of nodriver: an async-first, undetected browser-automation framework driving Chrome straight over the DevTools Protocol, no WebDriver in sight. It exists because nodriver's single maintainer rarely merged outside fixes, so the community forked to accept bug fixes, add features and take issues on GitHub. What it did not fix is authenticated proxies. This guide covers the full proxy setup, what genuinely differs from nodriver, and the workarounds that get user:pass working.

Install and basic proxy setup

Installation is one line — pip install zendriver — and the API mirrors nodriver almost symbol for symbol, so import zendriver as zd is often the only change when porting a script. An unauthenticated proxy goes through browser_args, and the request exits from the proxy IP:

import zendriver as zd

async def main():
    browser = await zd.start(
        browser_args=["--proxy-server=gate.quantumproxies.io:PORT"],
    )
    page = await browser.get("https://httpbin.org/ip")
    print(await page.get_content())   # shows the proxy exit IP
    await browser.stop()

zd.loop().run_until_complete(main())

That works because it is just a Chrome flag. Add credentials — --proxy-server=http://USER:PASS@host:port — and Chromium silently discards the USER:PASS@ part, the proxy replies 407, and a native login dialog appears that zendriver cannot fill. This is a Chrome limitation, not a zendriver bug, so no version bump will make the flag accept a password.

The zendriver proxy authentication gap

The gap is openly tracked in zendriver's issues — a feature request thread (#10) and a dedicated "Proxy with auth" issue (#208) — which is itself a difference worth noting: on nodriver the same question is buried in a discussion the maintainer answered once and moved on. One user on issue #10 sums up the state of play bluntly: the proxy server option has no way to authenticate, so they use a proxy extension instead and it works fine. That is the field-tested consensus, and it points straight at the same three fixes nodriver users rely on.

Fix 1: IP whitelisting (the simplest)

If your job runs from a machine with a stable public IP, skip credentials entirely. Register the egress IP in your provider dashboard and the gateway authenticates you by source address — the zendriver code stays the plain --proxy-server snippet above, with zero auth logic. Every QuantumProxies plan supports IP whitelisting alongside user:pass, which makes it the default recommendation whenever your IP is fixed. The one limit is that it authenticates a machine, not a script, so ephemeral runners and containers behind NAT need one of the next two methods.

Comparison of nodriver and zendriver showing shared CDP architecture and proxy auth gap but different maintenance models
zendriver keeps nodriver's stealth and API while adding an open issue tracker — the proxy auth gap, though, is inherited unchanged.

Fix 2: answer the challenge over CDP

Because zendriver exposes the DevTools Protocol the same way nodriver does, you can intercept the auth challenge in-process: register RequestPaused and AuthRequired handlers, then enable the Fetch domain with handle_auth_requests=True and reply with continue_with_auth. The two non-obvious rules are identical to nodriver — add the handlers before enabling the domain, and fire responses with asyncio.create_task so awaiting them cannot deadlock the loop:

import asyncio
import zendriver as zd

async def main():
    browser = await zd.start(browser_args=["--proxy-server=gate.quantumproxies.io:PORT"])
    tab = await browser.get("draft:,")            # blank tab first

    async def on_auth(event):
        asyncio.create_task(tab.send(zd.cdp.fetch.continue_with_auth(
            request_id=event.request_id,
            auth_challenge_response=zd.cdp.fetch.AuthChallengeResponse(
                response="ProvideCredentials", username="USER", password="PASS",
            ),
        )))

    async def on_request(event):
        asyncio.create_task(tab.send(
            zd.cdp.fetch.continue_request(request_id=event.request_id)))

    # handlers FIRST, then enable the domain
    tab.add_handler(zd.cdp.fetch.RequestPaused, on_request)
    tab.add_handler(zd.cdp.fetch.AuthRequired, on_auth)
    await tab.send(zd.cdp.fetch.enable(handle_auth_requests=True))

    page = await browser.get("https://httpbin.org/ip")
    await asyncio.sleep(3)
    print(await page.get_content())
    await browser.stop()

zd.loop().run_until_complete(main())

The full walkthrough of why the handler order matters, and what happens when you get it wrong, lives in our nodriver proxy authentication guide — the mechanics are shared, so there is no reason to reproduce them twice.

Fix 3: a proxy-auth extension, and SOCKS5

The route that issue #10 endorses is a generated Chrome extension: a Manifest V3 manifest plus a worker that sets the proxy and answers chrome.webRequest.onAuthRequired, loaded with --load-extension under --headless=new. It handles any proxy type, including SOCKS5, which matters because authenticated SOCKS5 never works through the flag — Chromium has no username/password support for SOCKS5 (Chromium bug 40829748). The alternative for SOCKS5 is a local relay that holds the credentials and offers a no-auth endpoint on 127.0.0.1, covered in the proxy relay guide. Every QuantumProxies plan ships both HTTP and SOCKS5 endpoints, so you can often sidestep the whole problem by using HTTP, which handles Basic auth cleanly.

What actually differs from nodriver

The fork is not cosmetic. In public benchmarks pitting nodriver, zendriver, Selenium and Playwright against modern anti-bot systems, the nodriver/zendriver family was the strongest at getting through, with zendriver edging ahead thanks to unmerged upstream fixes it carries. Practically, the differences that affect proxy work are: an active issue tracker where problems get triaged, a steadier release cadence, isolated browser contexts you can spin up per session, and batteries-included conveniences kept from nodriver. None of that closes the auth gap — but it means fixes land faster when they do, and it makes zendriver the easier fork to run many parallel sessions on. For rotating and pooling exits across those concurrent contexts, our notes on proxy pool management apply to zendriver unchanged, whether you route through rotating proxies or pin sticky sessions for logged-in flows.

One disambiguation: a separate Rust crate also named zendriver exists on docs.rs. It is unrelated to the Python fork discussed here — if you are scraping in Python, pip install zendriver is the one you want.

Flow of authenticating a zendriver proxy: install, start with the proxy flag, whitelist the IP, or fall back to a CDP auth handler
Whitelist when your egress IP is stable; answer the CDP challenge when it is not. The user:pass flag is a dead end either way.

Frequently asked questions

How do I use a proxy with zendriver?

Pass the address through browser_args when you call zendriver.start(): browser_args=["--proxy-server=host:port"]. That routes all traffic through the proxy for an unauthenticated endpoint. For an authenticated proxy you cannot put user:pass in the flag — whitelist your IP, use a CDP Fetch.AuthRequired handler, or load a proxy-auth extension.

Does zendriver support authenticated proxies?

Not through a built-in parameter — the gap is tracked in issues #10 and #208. Chromium ignores credentials in the proxy flag, so you authenticate another way: IP whitelisting at the provider, a CDP handler that answers the challenge in-process, a generated Chrome extension, or a local relay that holds the credentials for you.

What is the difference between nodriver and zendriver?

zendriver is a community-maintained fork of nodriver with the same CDP architecture, stealth goals and API. The difference is upkeep: zendriver takes issues and pull requests on GitHub, ships unmerged upstream bug fixes, and releases more regularly. Proxy authentication behaves identically in both — the fixes in this guide work for either.

Can zendriver use an authenticated SOCKS5 proxy?

Not via the flag, because Chromium never implemented SOCKS5 username/password auth (Chromium bug 40829748), and zendriver inherits that. Use a proxy-auth extension, run a local relay that adds the credentials, or point zendriver at your provider's HTTP endpoint instead — HTTP Basic proxy auth works reliably where SOCKS5 auth does not.

zendriver is the sharper of the two forks to build on today, but it hands you the same authenticated-proxy problem nodriver does. Whitelist when your IP is fixed, answer the CDP challenge when it is not, and keep the extension and relay as fallbacks. Whichever you choose, the exit IP does the heavy lifting — a maintained fork on a burned datacenter address still gets blocked.

Give zendriver clean residential exits