← All guides
DEVELOPER GUIDES · 6 MIN READ

Invisible Playwright Proxy Setup with Firefox, Auth, and Exit Checks

Route Invisible Playwright through an authenticated HTTP or SOCKS proxy, align the browser session with its egress, and validate the target result safely.

Invisible Playwright looks like Playwright at the call site, but its engine choice changes the proxy decision: the project documents a patched Firefox browser, not Playwright Chromium. That matters when you compare a browser fingerprint, a target response and a proxy route. The useful result is a repeatable browser session that reaches the intended target, exposes the expected fields and can be tied to a verified exit.

The package currently requires Python 3.11 or newer. Install it and fetch its browser once:

pip install invisible-playwright
python -m invisible_playwright fetch

The README lists Windows x86_64 and Linux x86_64/arm64 as supported platforms. fetch downloads the project’s browser binary; it is separate from choosing the runtime proxy.

Configure the proxy at the browser boundary

Invisible Playwright accepts a Playwright-style proxy dictionary. The documented fields are server, and optional username and password. The supported schemes are http, https, socks4 and socks5. Put the server, username and password in your runtime secret manager or environment. Keep credentials out of source files, page headers, URLs and logs.

Set the non-secret values below, replacing the reserved proxy hostname with your provider’s endpoint. Choose a sticky route for this sequence.

export PROXY_SERVER='http://proxy.example:3128'
export TARGET_URL='https://example.com/'
export EXPECTED_MARKER='Example Domain'

Inject PROXY_USER and PROXY_PASSWORD through your secret manager when authentication is required. Optional EXPECTED_EXIT_IP must be an independently known exit address; omit it if unknown. Optional BROWSER_TIMEZONE is an IANA zone. Save the script as proxy_check.py and run python proxy_check.py.

httpbin /ip supplies a public JSON echo with an origin string, so you need no private echo server. This example requires HTTP 200 and exactly one valid IP in that field; comma-separated forwarded chains fail rather than being guessed.

import json
import os
from ipaddress import ip_address
from urllib.parse import urlparse

from invisible_playwright import InvisiblePlaywright


def main():
    target_url = os.environ["TARGET_URL"]
    marker = os.environ["EXPECTED_MARKER"].strip()
    if not marker:
        raise ValueError("EXPECTED_MARKER must be nonempty")
    target = urlparse(target_url)
    if target.scheme != "https" or not target.hostname or target.username or target.password:
        raise ValueError("Use an HTTPS target without URL credentials")
    target_origin = (target.scheme, target.hostname, target.port or 443)

    server = os.environ["PROXY_SERVER"]
    parsed = urlparse(server)
    if (
        parsed.scheme not in {"http", "https", "socks4", "socks5"}
        or not parsed.hostname or not parsed.port
        or parsed.username or parsed.password
        or parsed.path not in {"", "/"} or parsed.query or parsed.fragment
    ):
        raise ValueError("Use a proxy scheme, host and port without credentials")
    proxy = {"server": server}
    user, password = os.getenv("PROXY_USER"), os.getenv("PROXY_PASSWORD")
    if bool(user) != bool(password):
        raise ValueError("Set both proxy credentials or neither")
    if user:
        proxy.update(username=user, password=password)
    expected = os.getenv("EXPECTED_EXIT_IP")
    expected_ip = ip_address(expected.strip()) if expected is not None else None
    options = {"proxy": proxy}
    if os.getenv("BROWSER_TIMEZONE"):
        options["timezone"] = os.environ["BROWSER_TIMEZONE"]

    with InvisiblePlaywright(**options) as browser:
        page = browser.new_page()
        echo_url = "https://httpbin.org/ip"
        echo = page.goto(echo_url, wait_until="load", timeout=20_000)
        if echo is None or echo.status != 200 or page.url != echo_url:
            raise RuntimeError("Exit check failed or redirected")
        payload = echo.json()
        origin = payload.get("origin") if isinstance(payload, dict) else None
        if not isinstance(origin, str) or not origin.strip():
            raise RuntimeError("Exit origin is missing")
        observed_ip = ip_address(origin.strip())
        if expected_ip is not None and observed_ip != expected_ip:
            raise RuntimeError("Exit differs from EXPECTED_EXIT_IP")

        response = page.goto(target_url, wait_until="domcontentloaded", timeout=20_000)
        final = urlparse(page.url)
        if response is None or response.status != 200:
            raise RuntimeError("Target status is not accepted")
        if (final.scheme, final.hostname, final.port or 443) != target_origin:
            raise RuntimeError("Target origin changed")
        body = page.locator("body").inner_text(timeout=5_000)
        if marker not in body:
            raise RuntimeError("Required target marker is missing")
        print(json.dumps({
            "observed_ip": str(observed_ip),
            "expected_exit_checked": expected_ip is not None,
            "target_status": response.status,
            "target_valid": True,
        }))


if __name__ == "__main__":
    try:
        main()
    except Exception:
        raise SystemExit("Check failed: inspect configuration, route and target") from None

The exit request runs before the target in the same page and browser session. Output contains only the parsed address and validation results; exceptions suppress raw connection details. The echo reports what httpbin observed, including forwarded-header information. It does not prove geography or that a rotating provider will reuse that IP for the target. Use a sticky route and reconcile provider records when continuity matters. The with block closes the browser on success or failure.

HTTP 200 plus a marker is a starting validator. For product data, require the actual title, price and currency fields before accepting a record.

Keep timezone and geography honest

The README says DNS is routed through the proxy by default. It also says the browser timezone is derived from the egress IP when a proxy is configured. An explicit IANA timezone passed as timezone= overrides that automatic value. This is the documented geo-related control: it does not select a country, city or proxy pool. The IP echo above supplies no location data. For regional work, check geography separately and validate the target’s language, currency or other required field.

Do not use a timezone override to hide a route mismatch. If the target expects a German page, for example, validate both the observed exit and the page’s required German field. A matching timezone alone is not proof that the proxy supplied the intended network geography.

Choose route type and session policy

Decision Prefer Verify
Network class is part of the acceptance rule Residential Supplier sourcing, observed exit and valid target fields
Server egress is sufficient for a permitted job Datacenter Target acceptance, cost per valid record and failure rate
Login, cookies or a multi-page record must stay coherent Sticky session Same exit and state across the flow
A fresh exit is itself the test variable Rotation New exit is recorded and the output is revalidated

Residential and datacenter are different network choices, not quality labels. The residential versus datacenter guide covers the sourcing and workload decision. For session lifetime and rotation triggers, see sticky sessions and rotation. Keep the browser session, proxy session identifier and target result in the same run record.

Read failures by boundary

Symptom Check first Next action
Browser does not start Python, platform and fetched engine Confirm Python 3.11+ and run python -m invisible_playwright version
407 or connection failure Proxy host, port and auth scheme Recheck secret binding and use the 407 guide
Exit is unexpected Endpoint, DNS and route assignment Compare the trusted exit check with provider records
Page loads without the required field Redirect, login, challenge or parser Record final URL and marker result before changing routes
Different pages disagree on locale Exit and timezone policy Validate egress and target fields together

For broader IP, DNS and browser checks, use the proxy diagnostics guide. If you are configuring ordinary Playwright rather than this patched Firefox wrapper, use the Playwright proxy setup. Keep the conclusion tied to the target, route and session that you actually validated.

Sources and further reading

Sign in ↗

Keep reading

Amazon Scraper API vs Proxy: Choose a Product Data Source

Distinguish authorized Amazon APIs, licensed product data and proxy-based page checks by record quality and access rights.

Read guide →

Apify Custom Proxy Setup: Use Your Own Residential Route

Connect a buyer-owned proxy to an Apify Actor, keep the session boundary clear, and validate records instead of counting requests.

Read guide →

Australia Residential Proxies: Verify the AU Exit and State-Level Result

Separate Australian egress from en-AU content, AUD pricing, GST display, postcode validation and the state delivery context.

Read guide →