Прокси в Playwright на Python: настройка browser и context, авторизация и проверка

11 мин чтения

Как задать HTTP-прокси в Playwright на Python на уровне browser и context, передать логин отдельно от origin, пройти CONNECT и проверить маршрут.

Текст для того, кто гоняет Playwright на Python через прокси и хочет увидеть маршрут в логе, а не только статус 200. Примеры с фиктивными учётными данными на loopback. Top Proxy не продаёт IP и не раздаёт бесплатные списки. Ниже сервисы из каталога, которые публикуют HTTP-доступ. Это не прогон чужого продакшена.

01Где задавать прокси: browser или отдельный context

Официальный раздел Network для Python на 14 октября 2026 говорит прямо. Страницы можно грузить через HTTP-прокси или SOCKSv5. Прокси задают глобально на весь browser либо отдельно на каждый context. Это сетевой прокси браузера. Это не Playwright MCP и не мок маршрутов page.route.

Лаборатория на этой машине шла на Python 3.14.6, Playwright 1.64.0 и Chromium 156.0.8078.4. Firefox в том же прогоне был 157.0. WebKit для номера сборки не запускали отдельно, только попытку SOCKS5 с логином. TLS не выключали. ignore_https_errors нигде не ставили в True.

  1. Если один и тот же выход нужен всем вкладкам процесса, передайте proxy в chromium.launch.
  2. Если два сценария должны идти разными выходами, откройте два browser.new_context с разными proxy.
  3. Context, у которого свой proxy, перекрывает значение, заданное на launch. В прогоне context ушёл на прокси B, а дефолтный прокси браузера этот запрос не видел.
  4. Прямой заход без прокси дал HTTP 200 и ноль событий на инструментированном прокси. Пока нет строки в логе прокси, статус 200 говорит только то, что кто-то ответил.

Документация browser_type.launch принимает server вида http://хост:порт или socks5://хост:порт. Короткая форма без схемы считается HTTP-прокси. То же поле есть у browser.new_context. Context по документации не делит cookies и кэш с соседним context. Это изоляция хранилища браузера, не допуск к аккаунту и не «чистота» IP.

Страница Мой IP в обычном окне показывает выход этого окна. Она не показывает маршрут Python и не показывает маршрут headless Chromium из скрипта.

02Host, порт и отдельная авторизация прокси

Поля username и password в объекте proxy в документации launch описаны как логин для HTTP-прокси, которому нужна аутентификация. Это не http_credentials. http_credentials на context это логин к origin, раздел HTTP Authentication на той же странице Network.

  1. Соберите server как http://127.0.0.1:порт без логина внутри URL, если пароль передаёте полями.
  2. Логин и пароль кладите в username и password. Спецсимволы в лаборатории были в пароле p@ss:/#%. Playwright принял их как отдельные поля, не как кусок URL.
  3. Не копируйте эту пару в http_credentials, пока не проверите, что делает ваш движок после 407.
  4. Сверьте лог прокси. Origin в лаборатории не получил заголовок Proxy-Authorization. Утечки на origin не было.

Chromium 156 после 407 на прокси сделал повтор и подставил пару из http_credentials в прокси-авторизацию. Первый хоп в логе был authReason=missing, второй уже authorized. Поля API по-прежнему разные. Полагаться на это как на задокументированный способ логина к прокси нельзя. Для предсказуемого прокси-логина используйте proxy.username и proxy.password.

Без логина HTTP-цель через прокси с Basic вернула страницу статуса 407 и тело Proxy Authentication Required, без исключения Python. Неверный пароль в этом же режиме тоже дал 407. Верная пара дала 200, и прокси записал authorized.

Для HTTPS-цели картина другая. CONNECT без логина и CONNECT с неверным паролем не превратились в страницу 407. page.goto("https://example.com/") упёрся в TimeoutError за 8000 мс, хотя прокси уже ответил 407 на туннель. Верный логин на CONNECT прошёл: статус 200 и цель example.com:443 в логе.

Поле bypass в документации это список доменов через запятую. В прогоне bypass="127.0.0.1" дал 200 на origin и ноль событий прокси. Тот же origin при bypass="example.com" снова пошёл через прокси. Это список исключений, не «белый список разрешённых сайтов».

03Исполняемый пример на Python и проверка маршрута

Ниже короткий самостоятельный скрипт. HTTP-origin и прокси слушают только 127.0.0.1. Затем тот же Chromium делает HTTPS CONNECT на example.com. Для этого шага нужна внешняя сеть. TLS проверки не отключает. Фиктивные данные. Это не полный harness и не прогон кабинета провайдера.

Рядом лежат tests/public_lab.py, tests/lab.py, tests/check_playwright.py и пин playwright==1.64.0. Полный прогон ставит Chromium, Firefox и WebKit, плюс openssl и cwebp. Относительный путь на диске не является публичной загрузкой.

#!/usr/bin/env python3
"""Читаемый пример: Playwright Python, HTTP-прокси и CONNECT.

Фиктивные учётные данные. HTTP-origin и прокси слушают только 127.0.0.1.
HTTPS CONNECT к example.com идёт во внешнюю сеть и требует доступ в интернет.
TLS остаётся включённым. Скрипт не ставит ignore_https_errors в True.

Это короткий самостоятельный пример, не полный harness из check_playwright.py.

    python3 -m pip install playwright==1.64.0
    python3 -m playwright install chromium
    python3 public_lab.py
"""
from __future__ import annotations

import base64
import json
import os
import re
import select
import socket
import ssl
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit

USER = "lab-user"
PASSWORD = "p@ss:" + "/#%"
REDACT = "***"
TIMEOUT_MS = 8000
STRIP = [
    "HTTP_PROXY",
    "HTTPS_PROXY",
    "ALL_PROXY",
    "NO_PROXY",
    "http_proxy",
    "https_proxy",
    "all_proxy",
    "no_proxy",
]
FAULT = os.environ.get("PLAYWRIGHT_RU_PUBLIC_FAULT", "")


_AUTH_JSON = re.compile(r'(?i)((?:proxy-)?authorization"\s*:\s*")([^"]*)')
_AUTH_HDR = re.compile(r'(?i)((?:proxy-)?authorization\s*[:=]\s*)([^\r\n,"\']+)')
_USERINFO = re.compile(r'(?i)((?:https?|socks5h?|socks4a?)://)([^/\s"\'@]+):([^/\s"\'@]+)@')
_BASIC = re.compile(r'(?i)(\bBasic\s+)([A-Za-z0-9+/=_-]{8,})')


def redact(s: object) -> str:
    text = str(s)
    text = _AUTH_JSON.sub(r"\1" + REDACT, text)
    text = _AUTH_HDR.sub(r"\1" + REDACT, text)
    text = _USERINFO.sub(r"\1***:***@", text)
    text = _BASIC.sub(r"\1" + REDACT, text)
    blob = base64.b64encode(f"{USER}:{PASSWORD}".encode("utf-8")).decode("ascii")
    return text.replace(PASSWORD, REDACT).replace(blob, REDACT)


def isolate_env() -> None:
    for key in STRIP:
        os.environ.pop(key, None)


def close_sock(sock: socket.socket | None) -> None:
    if sock is None:
        return
    try:
        sock.shutdown(socket.SHUT_RDWR)
    except OSError:
        pass
    try:
        sock.close()
    except OSError:
        pass


def leftover_listen(ports: list[int]) -> list[int]:
    still = []
    for port in ports:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(0.2)
        try:
            s.connect(("127.0.0.1", int(port)))
            still.append(int(port))
        except OSError:
            pass
        finally:
            s.close()
    return still


class Origin(BaseHTTPRequestHandler):
    def log_message(self, format: str, *args: object) -> None:  # noqa: A003
        return

    def do_GET(self) -> None:  # noqa: N802
        leaked = "proxy-authorization" in {k.lower() for k in self.headers.keys()}
        body = json.dumps({"ok": True, "path": self.path, "leakedProxyAuth": leaked}).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


class Tracked(ThreadingHTTPServer):
    daemon_threads = True

    def __init__(self, addr, handler):
        super().__init__(addr, handler)
        self.accepted: list[socket.socket] = []

    def get_request(self):
        req, addr = super().get_request()
        self.accepted.append(req)
        return req, addr


def start_origin() -> tuple[Tracked, threading.Event, threading.Thread]:
    if FAULT == "start":
        raise RuntimeError("inject-public-start")
    server = Tracked(("127.0.0.1", 0), Origin)
    stop = threading.Event()

    def loop() -> None:
        server.socket.settimeout(0.2)
        while not stop.is_set():
            try:
                server.handle_request()
            except OSError:
                if stop.is_set():
                    break

    t = threading.Thread(target=loop, name="public-origin", daemon=True)
    t.start()
    return server, stop, t


def start_proxy(require_auth: bool) -> dict:
    if FAULT == "proxy-start":
        raise RuntimeError("inject-public-proxy-start")
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(("127.0.0.1", 0))
    sock.listen(16)
    sock.settimeout(0.3)
    stop = threading.Event()
    events: list[dict] = []
    expected = "Basic " + base64.b64encode(f"{USER}:{PASSWORD}".encode("utf-8")).decode("ascii")

    def handle(conn: socket.socket) -> None:
        try:
            data = b""
            conn.settimeout(8)
            while b"\r\n\r\n" not in data:
                chunk = conn.recv(4096)
                if not chunk:
                    return
                data += chunk
            head = data.split(b"\r\n\r\n", 1)[0].decode("iso-8859-1", errors="replace")
            lines = head.split("\r\n")
            first = lines[0].split(" ")
            method = first[0] if first else ""
            target = first[1] if len(first) > 1 else ""
            headers = {}
            for line in lines[1:]:
                if ":" in line:
                    k, v = line.split(":", 1)
                    headers[k.strip().lower()] = v.strip()
            rec = {"method": method, "target": target, "authorized": False}
            if require_auth:
                got = headers.get("proxy-authorization")
                if got != expected:
                    rec["authReason"] = "missing" if not got else "wrong"
                    events.append(rec)
                    body = b"Proxy Authentication Required"
                    conn.sendall(
                        b"HTTP/1.1 407 Proxy Authentication Required\r\n"
                        b"Proxy-Authenticate: Basic realm=\"lab\"\r\n"
                        b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n\r\n" + body
                    )
                    return
                rec["authorized"] = True
            events.append(rec)
            if method.upper() == "CONNECT":
                host, port_s = target.rsplit(":", 1)
                dest = socket.create_connection((host, int(port_s)), timeout=5)
                conn.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
                halt = threading.Event()
                sockets = [conn, dest]
                while not halt.is_set() and not stop.is_set():
                    readable, _, _ = select.select(sockets, [], [], 0.2)
                    for src in readable:
                        chunk = src.recv(65536)
                        if not chunk:
                            halt.set()
                            break
                        dst = dest if src is conn else conn
                        dst.sendall(chunk)
                close_sock(dest)
                return
            parsed = urlsplit(target)
            dest = socket.create_connection((parsed.hostname, parsed.port or 80), timeout=5)
            path = parsed.path or "/"
            fwd = f"{method} {path} HTTP/1.1\r\nHost: {parsed.hostname}:{parsed.port or 80}\r\nConnection: close\r\n\r\n"
            dest.sendall(fwd.encode("ascii"))
            while True:
                chunk = dest.recv(65536)
                if not chunk:
                    break
                conn.sendall(chunk)
            close_sock(dest)
        except OSError:
            return
        finally:
            close_sock(conn)

    def accept() -> None:
        while not stop.is_set():
            try:
                conn, _ = sock.accept()
            except OSError:
                continue
            threading.Thread(target=handle, args=(conn,), daemon=True, name="public-proxy-client").start()

    t = threading.Thread(target=accept, name="public-proxy", daemon=True)
    t.start()
    return {
        "sock": sock,
        "stop": stop,
        "thread": t,
        "port": int(sock.getsockname()[1]),
        "events": events,
    }


def shutdown(origin, origin_stop, proxy) -> None:
    if FAULT == "shutdown":
        pass
    origin_stop.set()
    if proxy:
        proxy["stop"].set()
        close_sock(proxy["sock"])
    if origin:
        for acc in getattr(origin, "accepted", []):
            close_sock(acc)
        try:
            origin.server_close()
        except OSError:
            pass
        close_sock(getattr(origin, "socket", None))


def main() -> int:
    isolate_env()
    origin = None
    origin_stop = threading.Event()
    proxy = None
    browser = None
    pw = None
    ports: list[int] = []
    out: dict = {"cleaned": False, "leftoverListen": False, "operationError": None, "cases": []}
    try:
        if FAULT == "constructor":
            ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER).load_cert_chain("/no/such/cert.pem")
        origin, origin_stop, _ot = start_origin()
        origin_port = int(origin.server_address[1])
        ports.append(origin_port)
        proxy = start_proxy(require_auth=True)
        ports.append(proxy["port"])
        from playwright.sync_api import sync_playwright

        pw = sync_playwright().start()
        missing = pw.chromium.launch(
            timeout=30000,
            proxy={"server": f"http://127.0.0.1:{proxy['port']}"},
        )
        page = missing.new_page()
        page.set_default_timeout(TIMEOUT_MS)
        missing_status = None
        missing_err = None
        try:
            res = page.goto(f"http://127.0.0.1:{origin_port}/", wait_until="domcontentloaded")
            missing_status = res.status if res else None
        except Exception as e:  # noqa: BLE001
            missing_err = redact(f"{type(e).__name__}: {e}")
        missing.close()
        out["cases"].append({"name": "http-auth-missing", "status": missing_status, "error": missing_err})

        browser = pw.chromium.launch(
            timeout=30000,
            proxy={
                "server": f"http://127.0.0.1:{proxy['port']}",
                "username": USER,
                "password": PASSWORD,
            },
        )
        page = browser.new_page()
        page.set_default_timeout(TIMEOUT_MS)
        res = page.goto(f"http://127.0.0.1:{origin_port}/", wait_until="domcontentloaded")
        body = page.text_content("body")
        leaked = False
        try:
            leaked = bool(json.loads(body or "{}").get("leakedProxyAuth"))
        except json.JSONDecodeError:
            pass
        out["cases"].append(
            {
                "name": "http-auth-ok",
                "status": res.status if res else None,
                "body": body,
                "leakedProxyAuth": leaked,
            }
        )
        https_err = None
        https_status = None
        try:
            res_https = page.goto("https://example.com/", wait_until="domcontentloaded", timeout=TIMEOUT_MS)
            https_status = res_https.status if res_https else None
        except Exception as e:  # noqa: BLE001
            https_err = redact(f"{type(e).__name__}: {e}")
        connect_hits = [e for e in proxy["events"] if e.get("method") == "CONNECT"]
        out["cases"].append(
            {
                "name": "https-connect-example",
                "status": https_status,
                "error": https_err,
                "connectTargets": [e.get("target") for e in connect_hits],
            }
        )
        browser.close()
        browser = None
        pw.stop()
        pw = None
    except Exception as e:  # noqa: BLE001
        out["operationError"] = redact(f"{type(e).__name__}: {e}")
    finally:
        try:
            if browser is not None:
                browser.close()
        except Exception as e:  # noqa: BLE001
            out["operationError"] = out.get("operationError") or redact(str(e))
        try:
            if pw is not None:
                pw.stop()
        except Exception:
            pass
        shutdown(origin, origin_stop, proxy)
        time.sleep(0.3)
        left = leftover_listen(ports)
        out["leftoverListen"] = bool(left)
        out["cleaned"] = not left
        out["ports"] = ports
        print(json.dumps(out, ensure_ascii=False, indent=2))
    ok = bool(out["cleaned"]) and (bool(FAULT) or out.get("operationError") is None)
    return 0 if ok else 1


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as e:  # noqa: BLE001
        print(json.dumps({"cleaned": False, "operationError": redact(e)}, ensure_ascii=False))
        raise SystemExit(1)
  1. Для короткого примера поставьте Playwright 1.64.0 и Chromium. Для полного harness дополнительно поставьте Firefox и WebKit, плюс openssl и cwebp в PATH.
  2. Снимите HTTP_PROXY и соседние переменные, если выбираете прокси в коде.
  3. Задайте proxy.server на host и порт. Логин добавьте полями, не в URL origin.
  4. Сделайте page.goto на свой endpoint. Для HTTP смотрите absolute-form в логе прокси. Для HTTPS смотрите CONNECT хост:443.
  5. Сверьте тело origin. В читаемом прогоне leakedProxyAuth был false.
  6. Не путайте этот вывод со страницей проверки прокси. Чекер не заменяет трассу Playwright.
Таблица утверждений лаборатории Playwright Python 14 октября 2026

Снимок таблицы утверждений того же прогона: Playwright 1.64.0, Chromium 156.0.8078.4, Python 3.14.6. Это таблица результатов кода, не обложка статьи.

04HTTPS через HTTP-прокси: CONNECT и доверие сертификату

Ключ https в URL цели не требует, чтобы сам прокси слушал TLS. Клиент шлёт CONNECT хост:порт, прокси отвечает 200 Connection Established, дальше внутри туннеля идёт TLS к origin. В лаборатории CONNECT на example.com:443 через открытый HTTP-прокси дал страницу 200. Схема прокси оставалась http://127.0.0.1:порт.

Локальный HTTPS-origin был подписан короткоживущим CA с SAN на localhost и 127.0.0.1. Chromium без этой CA в своём доверии упал с net::ERR_CERT_AUTHORITY_INVALID. CONNECT к этому порту при этом в логе прокси был. Значит туннель собрали, а проверка сертификата отказала уже после CONNECT.

Python ssl с load_verify_locations на тот же CA.pem дал HTTP 200 и полное тело. Без CA тот же клиент получил SSLCertVerificationError. Это доверие стека Python, не доверие Chromium. Подсовывать verify из Requests в браузер нельзя.

Один ограниченный заход: Firefox persistent context с каталогом профиля внутри статьи и файлом lab-ca.pem в этом каталоге. У Playwright Python нет поля, которое импортирует CA в профиль. Дистрибутив браузера и системное хранилище не меняли. ignore_https_errors не включали. Локальный HTTPS в этом профиле не стал 200. Это не браузерный успех с лабораторным CA.

Не включайте ignore_https_errors. Это выключение проверки, а не установка CA.

Доверие Python без системного хранилища: evidence/ca.pem и ssl.create_default_context().load_verify_locations. Это другой стек. Успешный HTTPS в Chromium в этом прогоне это публичный сертификат example.com после CONNECT, не лабораторный CA. Эти две ветки не складываются в «браузер принял наш CA».

05Два context: маршруты, cookies и границы изоляции

Два context одного Chromium ходили на один origin разными прокси. У каждого свой Set-Cookie. Значения b8a34d162201 и 02a02220fa3c не совпали. Банки context.cookies() тоже разошлись. Документация и обещает, что context не делит cookies и кэш.

Это граница хранилища и маршрута. Это не проверка, «пройдёт ли логин» на чужом сайте, и не обещание, что цель не склеит отпечаток. Протокол прокси не равен типу IP. Датацентр, резидент и мобильный выход это другой разговор, его смотрите в каталоге мобильных и резидентных карточек.

Context с собственным proxy перекрыл launch. Запрос на origin B остался в логе прокси B. В логе дефолтного прокси браузера цели с портом origin B не было. Одновременно открытые страницы не смешали банки cookies. Если нужен общий jar, не плодите лишний context без причины. Изоляция context не заменяет отдельные процессы, когда нужен полный сброс профиля.

06SOCKS5: DNS и ограничения конкретного движка

Документация называет SOCKSv5 наравне с HTTP в поле server. Логин в тех же полях username и password описан для HTTP-прокси. На SOCKS5 с авторизацией это само не переносится.

Chromium 156 через socks5://127.0.0.1:порт без логина открыл origin с 200. Хендшейк на нашей стороне записал METHOD 0, CMD 1, ATYP 3, назначение 127.0.0.1:порт. ATYP 3 это доменное имя, даже когда строка выглядит как адрес. Это не socks5h из curl и не обещание, что DNS всегда на стороне прокси. Другой движок мы на этот хендшейк не обобщаем.

Запуск с socks5:// плюс username и password на Chromium, Firefox и WebKit оборвался одной фразой Playwright: Browser does not support socks5 proxy authentication. Навигации не было. Если провайдер отдаёт только SOCKS5 с логином, этот клиент 1.64.0 на трёх движках в нашем прогоне такой выход не взял.

Официальный текст launch отдельно уточняет, что username и password необязательны, если HTTP-прокси требует аутентификацию. Про SOCKS-логин там такой фразы нет. Наш прогон это совпал: HTTP Basic живёт, SOCKS5 с логином отвергается на старте браузера. Не переносите семантику curl socks5h:// на Playwright. Пока нет хендшейка на Firefox и WebKit без логина, их DNS и ATYP считайте непроверенными.

07Ошибки 407, отказ соединения и ограничение времени

Смотрите исключение и лог прокси вместе. Одно число timeout не описывает слой.

В коде лаборатории лимит навигации задан явно: page.goto(..., timeout=8000). Это timeout операции goto, не скрытый per-read у сокета прокси. Если прокси принял CONNECT и молчит, вы увидите тот же TimeoutError, что и на задержанном HTTP-теле. Если порт занят сокетом без listen, Chromium 156 в этом прогоне не ждал «вечно»: он вернул ERR_PROXY_CONNECTION_FAILED. Путать эти две ветки в мониторинге вредно. По ним чинят разные вещи: учётку, DNS, файрвол, живой процесс прокси.

Собственный сокет, который мы только bind и не listen, дал net::ERR_PROXY_CONNECTION_FAILED за 7866 мс. Это отказ на TCP к прокси, не TimeoutError. Отдельный origin, который принял соединение и молчал дольше лимита, дал TimeoutError: Page.goto: Timeout 8000ms exceeded, 8037 мс. Молчаливый TLS после рукопожатия в Python дал TimeoutError: The read operation timed out, 8016 мс. Это разные классы. Не называйте отказ таймаутом.

Настройка или случайОжидаемый маршрутНаблюдение 14.10.2026Ограничение
Прямой goto без proxyМимо проксиHTTP 200, 0 событий проксиLoopback, не провайдер
launch(proxy=http://…)GET absolute-form на прокси200, в логе GET на originChromium 156
Context proxy B при launch AТолько прокси B200, прокси A без этой целиОдин процесс, два context
HTTPS origin с лабораторным CACONNECT, затем отказ TLSERR_CERT_AUTHORITY_INVALID, CONNECT в логеCA не в доверии Chromium
HTTPS example.com через HTTP-проксиCONNECT host:443200, цель example.com:443Публичный сертификат
HTTP без логина на Basic-прокси407 на проксиСтатус 407, тело Proxy Authentication RequiredНе exception
CONNECT без логина или с неверным паролем407 на туннелеПрокси видит missing/wrong, goto даёт TimeoutError 8000 мсChromium не показал страницу 407
Верный логин HTTP и CONNECT200, authorizedOrigin без Proxy-AuthorizationФиктивная пара
bypass=127.0.0.1Мимо прокси200, 0 событийТолько перечисленные хосты
SOCKS5 без логинаSOCKS CONNECT200, ATYP 3, DST 127.0.0.1Только Chromium в этом хендшейке
SOCKS5 с логиномОтказ запускаBrowser does not support socks5 proxy authenticationChromium, Firefox, WebKit
Bind без listenОтказ к проксиERR_PROXY_CONNECTION_FAILED, 7866 мсНе TimeoutError
Задержанный ответ, timeout 8000Обрыв навигацииTimeoutError, 8037 мсЭто timeout API goto

Протокол не равен происхождению IP. GET 200 не доказывает страну, допуск к аккаунту и обход любой цели. Контрольные фикстуры не являются тестом Oxylabs или Decodo.

Официальные тексты продуктов описывают HTTP-прокси с логином и паролем. Quick Start Oxylabs от 14 октября 2026 даёт curl -x ENDPOINT:PORT -U "PREFIX-USERNAME:PASSWORD" и явно меняет префикс клиента на http://, https:// или socks5h://. Резидентный endpoint pr.oxylabs.io:7777, префикс логина customer-. Запросы user:pass Decodo пишут пару на gate.decodo.com:7000, логин всегда с префиксом user-. Методы авторизации Decodo отдельно фиксируют 407 при неверной паре. Карточки Oxylabs и Decodo в каталоге открывались. HTTP 200 каталога не доказывает протокол. Продакшен-гейт с Playwright мы не вызывали. Клиентский proxy.username в Playwright совпадает с документированным HTTP user:pass. Это не прогон кабинета. Цифры cookies и миллисекунд в тексте совпадают с сохранённым файлом results.json этого каталога, без нового сетевого запуска лаборатории.

Oxylabs — резидентские и датацентровые прокси
Oxylabs — резидентские и датацентровые прокси4.0770

Oxylabs продаёт резидентские, датацентровые и мобильные прокси. Резидентские планы от $6/GB (5 GB / мес). Оплата: карты, банковский перевод, AliPay, PayPal.

10 стран
0,59 $
за 1 GB
Decodo (Smartproxy) — резидентские и датацентровые прокси
Decodo (Smartproxy) — резидентские и датацентровые прокси4.22107

Decodo (ранее Smartproxy) продаёт резидентские и датацентровые прокси. Резидентские: $4/GB без месячного плана, подписки от $3.75/GB за 3 GB. Оплата: карты, PayPal, Alipay, Google/Apple Pay, крипто (с оговорками).

83 страны
0,38 $
за 1 GB

Где задавать прокси в Playwright?

Глобально в chromium.launch или отдельно в browser.new_context. Context со своим proxy перекрывает значение браузера. Это поле proxy, не MCP и не page.route.

Как передать логин и пароль?

Поля username и password объекта proxy. В документации launch они описаны для HTTP-прокси. Не путайте их с http_credentials origin. В Chromium 156 origin-пара после 407 иногда ушла в повтор прокси. Для предсказуемого входа задавайте логин в proxy.

Нужен ли HTTPS-прокси для HTTPS-сайта?

Нет. Обычный HTTP-прокси делает CONNECT, TLS идёт к origin внутри туннеля. В лаборатории https://example.com открылся через http://127.0.0.1:порт.

Как проверить маршрут Python browser?

Сверьте лог прокси. HTTP должен оставить GET в absolute-form. HTTPS должен оставить CONNECT хост:порт. Страница Мой IP в другом окне показывает другой процесс.

Поддерживается ли SOCKS5 с авторизацией?

Playwright 1.64.0 на Chromium, Firefox и WebKit отказал запуском Browser does not support socks5 proxy authentication. SOCKS5 без логина на Chromium 156 прошёл, ATYP 3. Другие движки на этот хендшейк не обобщаем.

Почему разные context идут разными маршрутами?

У каждого context свой proxy и своё хранилище cookies. В прогоне банки не совпали. Это не критерий допуска к аккаунту.

Чем отказ соединения отличается от таймаута?

Bind без listen дал ERR_PROXY_CONNECTION_FAILED. Origin, который принял сокет и молчал, дал TimeoutError за лимит goto 8000 мс. Смотрите className и elapsed, не одно слово «не открылось».