Proxy w Playwright: konfiguracja przeglądarki i kontekstu

12 min

Ustawić proxy Playwright na przeglądarce albo kontekście, przekazać dane logowania, potwierdzić HTTPS CONNECT i naprawić 407 oraz błędy certyfikatu bez wyłączania TLS.

Ten poradnik jest dla programistów, którzy potrzebują, żeby ruch Playwright wychodził przez proxy HTTP albo SOCKS5, albo dla całej przeglądarki, albo dla jednego kontekstu. Laboratorium używało Playwright 1.64.0 wobec lokalnego origin i atrapy proxy z CONNECT. Nie ma darmowych kluczy, a Top Proxy nie sprzedaje adresów IP. Zaliczona lokalna atrapa to nie konto produkcyjne ani region.

01Ustawić proxy na przeglądarce albo na kontekście

Playwright dokumentuje jeden obiekt proxy w dwóch miejscach. Network: HTTP proxy (dostęp 14 października 2026) mówi, że proxy można ustawić globalnie dla całej przeglądarki albo dla każdego kontekstu. browser.newContext() powtarza te same pola: server, opcjonalne bypass, opcjonalne username, opcjonalne password. Przewodnik sieciowy dla Pythona używa tego samego kształtu na chromium.launch(proxy=...) i browser.new_context(proxy=...). Ten opis wykonuje bibliotekę Node. Python jest w tamtej dokumentacji podobny pod względem API, nie drugim laboratorium.

Proxy kłaść na chromium.launch, gdy każdy kontekst w tym procesie ma dzielić jedno wyjście. Kłaść na browser.newContext, gdy jeden proces musi trzymać dwa wyjścia, dwa magazyny ciasteczek albo domyślne plus nadpisanie.

  1. Zdecydować, czy zadanie to „jedno wyjście na cały przebieg”, czy „dwie tożsamości w jednym procesie”.
  2. Jeśli zadanie to jedno wyjście dla całego procesu przeglądarki, ustawić proxy na chromium.launch. W Playwright Test use.proxy w playwright.config.ts jest fixture kontekstu: dotyczy kontekstu tego testu i jego stron, nie jako domyślna wartość launch współdzielona przez każdy kontekst przeglądarki. Zobaczyć Configuration (use).
  3. Jeśli zadanie to dwa wyjścia, uruchomić bez przypinania drugiej tożsamości, a potem przekazać inne proxy do każdego newContext.
  4. Zamknąć każdy utworzony kontekst przed browser.close(), co jest uwagą o sprzątaniu przy browser.newContext().

Laboratorium uruchomiło Chromium 156.0.8078.4 z proxy HTTP na poziomie przeglądarki do 127.0.0.1 i pobrało lokalny origin. Strona zwróciła HTTP 200. Log proxy pokazał GET http://127.0.0.1:<port>/. Po usunięciu z procesu HTTP_PROXY, HTTPS_PROXY, ALL_PROXY i NO_PROXY bezpośredni launch bez opcji proxy trafił w ten sam origin przy zerze zdarzeń proxy. To czysta linia bazowa bez proxy. Nie mierzy, co Playwright zrobiłby, gdyby te zmienne nadal były ustawione.

Druga przeglądarka użyła pierwszego proxy jako domyślnego przy launch, a potem otworzyła kontekst, którego proxy.server wskazywał inny lokalny forwarder. Żądanie kontekstu zwróciło 200. Trafienia wylądowały na drugim forwarderze. Pierwszy forwarder nie zobaczył URL origin tego kontekstu. proxy kontekstu zastąpiło domyślne przeglądarki dla tego kontekstu. Nie scalało się z nim.

Playwright Test use.proxy konfiguruje kontekst testu i jego strony. chromium.launch({ proxy }) to osobne ustawienie domyślne przeglądarki. To nie API rotacji per test. W API 1.64.0, które to laboratorium wykonało, nie ma pola rotateSession na launch ani na newContext.

02Przekazać host, port i dane logowania bez ich wycieku

server to URL. Tekst Browser API jest wprost: obsługiwane są http://myproxy.com:3128 i socks5://myproxy.com:3128. Krótka forma myproxy.com:3128 jest traktowana jako HTTP. Jeśli dostawca dał punkt SOCKS5, schemat socks5:// jest wymagany. Nazwa użytkownika i hasło są udokumentowane jako dane logowania dla proxy HTTP, które wymaga uwierzytelniania. Nie są udokumentowane jako userpass SOCKS5.

Hasło trzymać w środowisku albo w magazynie sekretów. Nie wklejać go do committowanego configu. Playwright i tak musi wysłać Proxy-Authorization na drucie. Laboratorium używało atrapy użytkownika lab-user i atrapy hasła, które zawiera @, :, /, # oraz %. Logi wyników redagowały tę wartość.

Uwierzytelnianie proxy HTTP w Chromium jest gadatliwe. Wobec lokalnego proxy Basic:

  1. Ustawić server na http://127.0.0.1:<port> bez nazwy użytkownika. page.goto na origin HTTP zwróciło status 407. Log proxy zapisał authReason: missing. Playwright nie rzucił. Status na obiekcie odpowiedzi był dowodem.
  2. Ustawić użytkownika lab-user i złe hasło. Znowu status 407. Log pokazał pierwszą sondę bez danych, potem authReason: wrong.
  3. Ustawić atrapę hasła poprawnie. Status 200. Log pokazał tę samą sondę bez danych, potem authorized: true.

Złe dane logowania w tej kompilacji Chromium to nie wiszące gniazdo. To HTTP 407 od proxy, wyświetlone jako status nawigacji. Jeśli wrapper później rzuca, zachować 407 i klasę wyjątku. Nie spłaszczać obu do „proxy nie działa”.

HTTP Authentication na tej samej stronie Network to httpCredentials dla origin, nie proxy.username. Mieszanie ich to częsta pomyłka: origin 401 i proxy 407 to różne przeskoki.

03Potwierdzić, że ruch HTTPS używa CONNECT przez proxy HTTP

Schemat URL celu i schemat URL proxy to różne pokrętła. Origin https://example.com/ nadal używa proxy HTTP na http://127.0.0.1:<port>. Metoda proxy to CONNECT, cel example.com:443. Laboratorium zapisało ten CONNECT, potem Chromium dokończyło TLS wobec publicznego certyfikatu, a page.goto zwróciło 200. To HTTPS przez proxy HTTP przy włączonej weryfikacji.

Lokalny origin HTTPS podpisany laboratoryjnym CA to druga połowa. Chromium 156.0.8078.4, ignoreHTTPSErrors zostawione na udokumentowanym domyślnym false, nawigowało do https://127.0.0.1:<https-port>/ przez to samo proxy HTTP. Proxy zapisało CONNECT 127.0.0.1:<https-port>. Potem Playwright rzucił PlaywrightError z net::ERR_CERT_AUTHORITY_INVALID. CONNECT się wydarzył. TLS i tak padł. To, że cel był https://, nie zamieniło proxy w „proxy https”, a to, że proxy przyjęło CONNECT, nie sprawiło, że Chromium zaufało prywatnemu CA.

Node https.request do tego samego origin z rejectUnauthorized: true i bez CA padło z unable to verify the first certificate. Ten sam klient z PEM laboratoryjnego CA zwrócił HTTP 200. To udokumentowana ścieżka zaufania Node. Opcja Playwright clientCertificates, dodana w v1.46 na newContext, to certyfikat klienta dla nazwanego origin, nie sposób na instalację prywatnego CA. Udokumentowany przełącznik Playwright, który pominąłby tę awarię, to ignoreHTTPSErrors: true. Ten artykuł go nie używa. Nie ustawiać też rejectUnauthorized: false.

Jeśli zadanie to „udowodnić CONNECT”, czytać log proxy pod CONNECT host:port. Jeśli zadanie to „udowodnić, że strona się załadowała”, nadal potrzebny jest certyfikat, któremu Chromium już ufa, albo CA zainstalowane procesem poza tym przewodnikiem. Strona sprawdzania w osobistej przeglądarce to wyjście tej przeglądarki, nie trasa Playwright. Jaki jest mój adres IP używać tylko jako pomocnika wyjścia przeglądarki. Sprawdzanie proxy pokazuje, czy host przyjmuje HTTP albo SOCKS5, zanim Playwright w niego wskazać.

Tabela asercji z laboratorium loopback Playwright 1.64.0 z 14 października 2026

Rysunek to tabela wyników z tego przebiegu, nie okładka 16:9 i nie zrzut sprawdzania.

04Używać SOCKS5 tylko tam, gdzie silnik naprawdę to obsługuje

Przewodnik Network mówi, że strony mogą ładować się przez proxy HTTP(S) albo SOCKSv5. Chromium 156.0.8078.4 z server: "socks5://127.0.0.1:<port>" i bez nazwy użytkownika pobrało origin HTTP ze statusem 200. Atrapa SOCKS zapisała CONNECT 127.0.0.1:<origin-port>. Nieuwierzytelnione SOCKS5 jest w tym opisie dla tego przypiętego Chromium.

Uwierzytelnione SOCKS5 nie. chromium.launch, firefox.launch i webkit.launch w Playwright 1.64.0 wszystkie rzuciły PlaywrightError: Browser does not support socks5 proxy authentication. Rzut nastąpił przy launch, przed page.goto. Playwright 1.64.0 pobrał Firefox 157.0 i WebKit 27.2 na te uruchomienia. Komunikat zgadza się z dawno otwartą prośbą produktową microsoft/playwright#10567, nadal Open z etykietą P3-collecting-feedback 14 października 2026. Komentarz maintainera z 2021 zapisuje tam, że Chromium i Firefox nie mają userpass SOCKS5 w samych silnikach.

Jeśli dostawca oferuje wyłącznie SOCKS5 z nazwą użytkownika, ta przypięta kompilacja Playwright tego użytkownika nie wyśle. Użyć proxy HTTP z username i password albo nieuwierzytelnionego punktu SOCKS5, który silnik naprawdę wykręcił w teście takim jak poniżej. Porównanie protokołów jest w SOCKS5 a HTTP. Python Requests to inny klient i inny artykuł: Proxy w Python requests.

05Omijać wybrane hosty bez traktowania bypass jako uniwersalnej ucieczki

bypass to lista domen rozdzielona przecinkami. Przykład API to ".com, chromium.org, .domain.com". To nie wyłącznik dla każdego hosta i nie funkcja prywatności.

  1. Uruchomić Chromium z proxy.server wskazującym otwarty forwarder i bypass: "127.0.0.1".
  2. Nawigować do http://127.0.0.1:<origin>/. Strona zwróciła 200. Log proxy dla tego okna był pusty. Wymieniony host pominął proxy.
  3. Uruchomić znowu z bypass: "example.com" i nawigować do tego samego origin 127.0.0.1. Status 200, a proxy zapisało GET http://127.0.0.1:<origin>/. Niewymieniony host nadal użył proxy.

To cała obietnica. bypass nie znaczy „to proxy w ogóle mnie nie widzi”. Znaczy „te hosty, w tym kontekście, nie są wysyłane do tego proxy”. Reguły localhost w Chromium potrafią zaskoczyć w innych kompilacjach. Mierzyć log proxy. Nie zakładać.

06Izolować dwa konteksty, żeby ciasteczka i wyjścia się nie mieszały

browser.newContext() tworzy magazyn, który nie dzieli ciasteczek z innymi kontekstami. Połączyć to z proxy per kontekst, gdy dwa testy nie mogą wyciec sesji na złe wyjście.

  1. Uruchomić Chromium bez proxy na poziomie przeglądarki.
  2. Otworzyć kontekst A z proxy A, kontekst B z proxy B.
  3. Nawigować oba do tego samego origin, który ustawia ciasteczko lab.
  4. Odczytać context.cookies().

Laboratorium dostało ciasteczko t-origin-a-67a984 w kontekście A i t-origin-a-ff4919 w kontekście B. Magazyny nie były równe. Obie nawigacje zwróciły 200. Izolacja tutaj to izolacja kontekstu Playwright plus dwa serwery proxy. To nie marketing sticky-session dostawcy i nie twierdzenie, że pula rezydencka utrzyma ten sam adres IP.

Zamykać otwarte konteksty. Wycieknięty kontekst trzyma ciasteczka i dane logowania proxy przy życiu, aż umrze przeglądarka.

07Diagnozować odmowy połączenia, 407 i awarie certyfikatu bez wyłączania TLS

Gdy Playwright „nie działa”, nazwać przeskok, który padł.

GdzieProtokółCo zaobserwowało laboratorium 1.64.0Limit silnika
Bezpośredni chromium.launch()brakorigin HTTP 200, zero zdarzeń proxybrak
proxy.server HTTP na poziomie przeglądarkiHTTP GET200, log proxy pokazuje GET w formie absolutnejbrak
proxy kontekstu nadpisujące launchHTTP GET200 na proxy kontekstu, nie na proxy launchbrak
Proxy HTTP, brakujące albo złe BasicHTTP 407status page.goto 407, bez rzutubrak
Proxy HTTP, poprawne BasicHTTP GET200 po sondzie Chromium bez danychbrak
Proxy HTTP, origin HTTPS z publicznym CACONNECT, potem TLSCONNECT example.com:443, strona 200brak
Proxy HTTP, origin HTTPS z laboratoryjnym CACONNECT, potem TLSCONNECT zapisany, potem net::ERR_CERT_AUTHORITY_INVALIDbrak API pliku CA na kontekście
SOCKS5, bez nazwy użytkownikaSOCKS CONNECTChromium 200, CONNECT atrapyauth nie bierze udziału
SOCKS5 plus nazwa użytkownikaodrzucone przy launchBrowser does not support socks5 proxy authentication na Chromium, Firefox, WebKitPlaywright 1.64.0 + silniki
proxy.server http://127.0.0.1:1TCPnet::ERR_PROXY_CONNECTION_FAILEDbrak
bypass: "127.0.0.1"pominięcieorigin 200, zero zdarzeń proxylista jest dokładna, nie globalna

Odmówione proxy: Chromium i tak się uruchomiło. page.goto potem rzuciło PlaywrightError z net::ERR_PROXY_CONNECTION_FAILED. Domyślny limit czasu nawigacji w laboratorium wynosił 8000 ms. Przypadek odmowy padł szybciej niż ten limit. Zachować nazwę klasy PlaywrightError i kod net::. Nie przemianowywać tego na ogólny timeout.

Awaria certyfikatu: zostawić TLS włączony. Ścieżka lokalnego CA, która przeszła, to Node https z ustawionym ca. Ścieżka przeglądarki Playwright, która przeszła, to https://example.com/ przez CONNECT, który już ma publiczne CA. Ścieżka przeglądarki Playwright, która padła, to laboratoryjny origin. ignoreHTTPSErrors zostaje false.

Jak sprawdzać trasę, w tej kolejności:

  1. Potwierdzić, że port proxy przyjmuje połączenie. Odmówiony port to ERR_PROXY_CONNECTION_FAILED, nie złe hasło.
  2. Dla proxy HTTP patrzeć na 407 kontra 200. Brakujące i złe hasła były 407 w tym Chromium.
  3. Dla celów HTTPS szukać CONNECT w logu proxy, a potem patrzeć na TLS. CONNECT plus ERR_CERT_AUTHORITY_INVALID znaczy, że tunel działał, a certyfikat nie.
  4. Dla SOCKS5 z nazwą użytkownika oczekiwać błędu launch w 1.64.0 raczej niż błędu strony.
  5. Dopiero potem patrzeć na ciało origin.

Publiczne laboratorium poniżej to atrapa widoczna dla czytelnika. Trzyma origin, proxy CONNECT i proces Chromium i zamyka je w finally, w tym przy awariach konstruktora i startu. Zapisać jako public_lab.mjs.

npm install playwright@1.64.0
npx playwright install chromium
node public_lab.mjs
#!/usr/bin/env node
/**
 * Self-contained loopback lab for Playwright 1.64.0 (Node).
 * Dummy credentials only. Local origin and CONNECT proxy. Does not call a paid provider.
 *
 *   npm install playwright@1.64.0
 *   npx playwright install chromium
 *   node public_lab.mjs
 *
 * TLS stays verified. This script never sets ignoreHTTPSErrors.
 */
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import http from "node:http";
import https from "node:https";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright";

const USER = "lab-user";
const PASSWORD = "p@ss:/#%";
const REDACT = "***";
const TIMEOUT_MS = 8000;
const SHUTDOWN_MS = 4000;
const STRIP = [
  "HTTP_PROXY",
  "HTTPS_PROXY",
  "ALL_PROXY",
  "NO_PROXY",
  "http_proxy",
  "https_proxy",
  "all_proxy",
  "no_proxy",
];

function redact(s) {
  return String(s)
    .split(PASSWORD)
    .join(REDACT)
    .split(Buffer.from(`${USER}:${PASSWORD}`).toString("base64"))
    .join(REDACT);
}

function isolateEnv() {
  for (const k of STRIP) delete process.env[k];
}

function basic() {
  return "Basic " + Buffer.from(`${USER}:${PASSWORD}`, "utf8").toString("base64");
}

function listen(server, sockets) {
  server._nativeClose = server.close.bind(server);
  server.on("connection", (s) => {
    sockets.push(s);
    s.on("error", () => {});
  });
  return new Promise((resolve, reject) => {
    server.once("error", reject);
    server.listen(0, "127.0.0.1", () => resolve(server.address().port));
  });
}

function closeServer(server) {
  const native = server._nativeClose || server.close.bind(server);
  const attempt = (fn) =>
    new Promise((resolve, reject) => {
      let settled = false;
      const done = (err) => {
        if (settled) return;
        settled = true;
        clearTimeout(t);
        if (err) reject(err);
        else resolve();
      };
      const t = setTimeout(() => done(new Error("server-close-timeout")), SHUTDOWN_MS);
      try {
        fn((err) => done(err || null));
      } catch (e) {
        done(e);
      }
    });
  return (async () => {
    try {
      if (typeof server.closeAllConnections === "function") server.closeAllConnections();
    } catch {}
    let lastErr = null;
    try {
      await attempt((cb) => server.close(cb));
    } catch (e) {
      lastErr = e;
      try {
        if (typeof server.closeAllConnections === "function") server.closeAllConnections();
      } catch {}
      try {
        await attempt((cb) => native(cb));
        lastErr = lastErr;
      } catch (e2) {
        lastErr = e2;
      }
    }
    if (server.listening) throw lastErr || new Error("server-still-listening");
    if (lastErr) throw lastErr;
  })();
}

async function guarded(name, fn, errors) {
  try {
    await Promise.race([
      fn(),
      new Promise((_, reject) => setTimeout(() => reject(new Error(`${name}-shutdown-bound`)), SHUTDOWN_MS)),
    ]);
  } catch (e) {
    errors.push(`${name}: ${e && e.message ? e.message : e}`);
  }
}

function makeCerts(tmp) {
  const caKey = path.join(tmp, "ca.key");
  const caPem = path.join(tmp, "ca.pem");
  const originKey = path.join(tmp, "origin.key");
  const originCsr = path.join(tmp, "origin.csr");
  const originPem = path.join(tmp, "origin.pem");
  const ext = path.join(tmp, "ext.cnf");
  fs.writeFileSync(ext, "subjectAltName=IP:127.0.0.1,DNS:localhost\nbasicConstraints=CA:FALSE\n");
  const run = (args) => {
    const r = spawnSync("openssl", args, { encoding: "utf8" });
    if (r.status !== 0) throw new Error(r.stderr || r.stdout);
  };
  run(["req", "-x509", "-newkey", "rsa:2048", "-keyout", caKey, "-out", caPem, "-days", "1", "-nodes", "-subj", "/CN=pw-public-lab-ca"]);
  run(["req", "-newkey", "rsa:2048", "-keyout", originKey, "-out", originCsr, "-nodes", "-subj", "/CN=127.0.0.1"]);
  run(["x509", "-req", "-in", originCsr, "-CA", caPem, "-CAkey", caKey, "-CAcreateserial", "-out", originPem, "-days", "1", "-extfile", ext]);
  return {
    caPem: fs.readFileSync(caPem, "utf8"),
    originKey: fs.readFileSync(originKey),
    originPem: fs.readFileSync(originPem),
  };
}

function startHttpOrigin(sockets) {
  const server = http.createServer((req, res) => {
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify({ ok: true, path: req.url, via: "http-origin" }));
  });
  return listen(server, sockets).then((port) => ({ server, port }));
}

function startHttpsOrigin(certs, sockets) {
  const server = https.createServer(
    { key: certs.originKey, cert: certs.originPem, minVersion: "TLSv1.2" },
    (req, res) => {
      res.setHeader("Content-Type", "application/json");
      res.end(JSON.stringify({ ok: true, path: req.url, via: "https-origin" }));
    },
  );
  return listen(server, sockets).then((port) => ({ server, port }));
}

function startConnectProxy(sockets) {
  const events = [];
  const server = net.createServer((socket) => {
    let buf = Buffer.alloc(0);
    const onData = (chunk) => {
      buf = Buffer.concat([buf, chunk]);
      const text = buf.toString("latin1");
      const split = text.indexOf("\r\n\r\n");
      if (split < 0) return;
      socket.removeListener("data", onData);
      const head = text.slice(0, split);
      const lines = head.split("\r\n");
      const [method, target] = lines[0].split(" ");
      const headers = {};
      for (const line of lines.slice(1)) {
        const c = line.indexOf(":");
        if (c >= 0) headers[line.slice(0, c).trim().toLowerCase()] = line.slice(c + 1).trim();
      }
      const rec = { method, target, auth: headers["proxy-authorization"] === basic() };
      events.push(rec);
      if (!rec.auth) {
        socket.end("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"lab\"\r\nContent-Length: 0\r\n\r\n");
        return;
      }
      if (method === "CONNECT") {
        const [host, portStr] = target.split(":");
        const dest = net.connect(Number(portStr), host, () => {
          socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
          socket.pipe(dest);
          dest.pipe(socket);
        });
        sockets.push(dest);
        dest.on("error", () => socket.destroy());
        return;
      }
      let url;
      try {
        url = new URL(target);
      } catch {
        socket.end("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n");
        return;
      }
      const req = http.request(
        {
          hostname: url.hostname,
          port: url.port || 80,
          path: url.pathname + url.search,
          method,
          headers: { host: url.host },
        },
        (res) => {
          socket.write(`HTTP/1.1 ${res.statusCode} ${res.statusMessage}\r\n`);
          for (const [k, v] of Object.entries(res.headers)) socket.write(`${k}: ${v}\r\n`);
          socket.write("\r\n");
          res.pipe(socket);
        },
      );
      req.on("error", () => socket.destroy());
      req.end();
    };
    socket.on("data", onData);
    socket.on("error", () => {});
  });
  return listen(server, sockets).then((port) => ({ server, port, events }));
}

function nodeHttpsWithCa(port, caPem, timeoutMs = TIMEOUT_MS) {
  return new Promise((resolve, reject) => {
    let settled = false;
    let deadline;
    const finish = (fn, arg) => {
      if (settled) return;
      settled = true;
      clearTimeout(deadline);
      fn(arg);
    };
    const failTimeout = () => {
      const err = new Error("node-https-timeout");
      err.name = "NodeHttpsTimeoutError";
      try {
        req.destroy(err);
      } catch {}
      finish(reject, err);
    };
    const req = https.request(
      {
        hostname: "localhost",
        port,
        path: "/",
        method: "GET",
        ca: caPem,
        rejectUnauthorized: true,
        timeout: timeoutMs,
      },
      (res) => {
        const chunks = [];
        res.on("data", (c) => chunks.push(c));
        res.on("end", () => finish(resolve, { status: res.statusCode, body: Buffer.concat(chunks).toString("utf8") }));
        res.on("error", (e) => finish(reject, e));
      },
    );
    req.on("timeout", failTimeout);
    req.on("error", (e) => finish(reject, e));
    deadline = setTimeout(failTimeout, timeoutMs);
    req.end();
  });
}

async function main() {
  isolateEnv();
  const owned = [];
  const sockets = [];
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pw-public-lab-"));
  let browser;
  const result = { ok: false, ignoreHTTPSErrors: false, cases: [] };
  const wipeTmp = () => {
    if (!tmp || !fs.existsSync(tmp)) return;
    for (const f of fs.readdirSync(tmp)) {
      if (f.endsWith(".key") || f.endsWith(".pem") || f.endsWith(".csr") || f.endsWith(".srl") || f.endsWith(".cnf")) {
        fs.rmSync(path.join(tmp, f), { force: true });
      }
    }
    fs.rmSync(tmp, { recursive: true, force: true });
  };
  try {
    const certs = makeCerts(tmp);
    const httpOrigin = await startHttpOrigin(sockets);
    owned.push(httpOrigin.server);
    const httpsOrigin = await startHttpsOrigin(certs, sockets);
    owned.push(httpsOrigin.server);
    const proxy = await startConnectProxy(sockets);
    owned.push(proxy.server);

    browser = await chromium.launch({
      proxy: {
        server: `http://127.0.0.1:${proxy.port}`,
        username: USER,
        password: PASSWORD,
      },
      timeout: 30000,
    });
    const context = await browser.newContext();
    const page = await context.newPage();
    page.setDefaultTimeout(TIMEOUT_MS);

    const httpRes = await page.goto(`http://127.0.0.1:${httpOrigin.port}/`);
    const httpBody = await page.textContent("body");
    result.cases.push({
      name: "browser-http-via-proxy",
      status: httpRes.status(),
      body: httpBody,
      proxyEvents: proxy.events.filter((e) => e.method === "GET").map((e) => ({ method: e.method, target: e.target })),
    });

    let tlsErr = null;
    try {
      await page.goto(`https://127.0.0.1:${httpsOrigin.port}/`);
    } catch (e) {
      tlsErr = { name: e.name, message: redact(e.message) };
    }
    result.cases.push({
      name: "browser-https-untrusted-ca",
      error: tlsErr,
      connectEvents: proxy.events.filter((e) => e.method === "CONNECT"),
    });

    const trusted = await nodeHttpsWithCa(httpsOrigin.port, certs.caPem);
    result.cases.push({
      name: "node-https-with-lab-ca",
      status: trusted.status,
      body: trusted.body,
    });

    await context.close();
    result.ok =
      result.cases[0].status === 200 &&
      tlsErr != null &&
      trusted.status === 200 &&
      result.cases[1].connectEvents.length >= 1;
    result.httpOrigin = httpOrigin.port;
    result.httpsOrigin = httpsOrigin.port;
    result.proxyPort = proxy.port;
  } finally {
    const errors = [];
    try {
      await guarded(
        "browser",
        async () => {
          if (browser) await browser.close();
        },
        errors,
      );
      await guarded(
        "sockets",
        async () => {
          for (const s of sockets.splice(0)) {
            try {
              s.destroy();
            } catch {}
          }
        },
        errors,
      );
      await guarded(
        "servers",
        async () => {
          const leftover = [];
          for (const s of owned) {
            try {
              await closeServer(s);
            } catch (e) {
              errors.push(`server: ${e && e.message ? e.message : e}`);
            }
            if (s.listening) leftover.push(s.address() && s.address().port);
          }
          if (leftover.length) throw new Error(`servers-still-listening:${leftover.join(",")}`);
        },
        errors,
      );
    } finally {
      await guarded("temp-keys", async () => wipeTmp(), errors);
    }
    if (errors.length) result.cleanupErrors = errors;
    result.serversLeftover = owned.filter((s) => s.listening).map((s) => s.address() && s.address().port);
  }
  console.log(JSON.stringify(result, null, 2));
  if (!result.ok || (result.serversLeftover && result.serversLeftover.length) || (result.cleanupErrors && result.cleanupErrors.some((e) => String(e).includes("still-listening")))) {
    process.exitCode = 1;
  }
}

export { nodeHttpsWithCa, TIMEOUT_MS, makeCerts, SHUTDOWN_MS, closeServer };

const isMain =
  process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isMain) {
  main().catch((e) => {
    console.error(redact(e && e.stack ? e.stack : e));
    process.exit(1);
  });
}

14 października 2026 ten skrypt zwrócił ok: true, HTTP 200 przez proxy, CONNECT do lokalnego portu HTTPS, net::ERR_CERT_AUTHORITY_INVALID w Chromium oraz HTTP 200 z Node z laboratoryjnym CA. Wyłącznie dane atrapy. openssl musi być na PATH.

Jeśli po działającym kliencie potrzebny jest dostawca, angielskie strony katalogu Bright Data, Oxylabs, SOAX i Decodo (Smartproxy) zwracały HTTP 200 14 października 2026 i wspominają HTTP oraz SOCKS5. Te strony nie były wyjściem laboratorium. Dopasowanie katalogu to opublikowany tekst protokołu, nie logowanie Playwright przez tego sprzedawcę.

Bright Data — proxy rezydenckie i datacenter
Bright Data — proxy rezydenckie i datacenter4.41442

Bright Data to duża sieć proxy do zbierania danych: IP rezydenckie, mobilne, ISP i datacenter w około 195 krajach. Ruch rezydencki można płacić na bieżąco bez subskrypcji, a adresy datacenter i ISP przychodzą w pakietach od 10 IP. Dostęp rezydencki i mobilny wymaga weryfikacji tożsamości.

10 krajów
5,00 USD
za 1 GB
Oxylabs — proxy rezydenckie i datacenter
Oxylabs — proxy rezydenckie i datacenter4.0771

Oxylabs sprzedaje proxy rezydenckie, mobilne, ISP i datacenter w jednej subskrypcji miesięcznej od $30. Płatności: karty, przelew, AliPay, PayPal.

10 krajów
0,45 USD
za 1 GB
SOAX — proxy rezydenckie i mobilne
SOAX — proxy rezydenckie i mobilne3.8218

SOAX sprzedaje rotacyjne proxy rezydenckie i mobilne na wspólnej puli kredytów, z datacenter i ISP w tych samych planach. Builder od $200/mies. + VAT (200 kredytów); ruch Tier 1 to $3/GB na Builderze.

85 krajów
3,00 USD
za 1 GB
Decodo (Smartproxy) — proxy rezydenckie i datacenter
Decodo (Smartproxy) — proxy rezydenckie i datacenter4.22118

Decodo (dawniej Smartproxy) sprzedaje proxy rezydenckie i datacenter. Rezydenckie: $4/GB bez planu miesięcznego albo pakiety miesięczne od $3.75/GB za 3 GB. Płatności: karty, PayPal, Alipay, Google/Apple Pay, krypto (z ograniczeniami).

83 kraje
0,38 USD
za 1 GB

Do wstępnego sprawdzenia hosta, portu i protokołu, zanim podepniecie Playwright, użyjcie sprawdzania proxy. Do głębszego sprawdzenia, czy to proxy wycieka, zobaczcie Jak sprawdzić, czy proxy działa.

Czy proxy ma siedzieć na przeglądarce, czy na kontekście?

Proxy na poziomie przeglądarki pokrywa każdy kontekst w tej przeglądarce. Proxy na poziomie kontekstu dotyczy jednego magazynu ciasteczek i może zastąpić domyślne przeglądarki. Laboratorium widziało, jak proxy kontekstu wysyła ruch do drugiego forwardera, podczas gdy proxy launch milczało.

Czy Playwright wysyła HTTPS przez proxy HTTP?

Tak. URL proxy zostaje http://host:port. Chromium wtedy wysyła CONNECT na host:443. Laboratorium zapisało CONNECT example.com:443 i dostało HTTP 200 przy włączonej weryfikacji TLS.

Jak przekazać uwierzytelnianie proxy?

Ustawić username i password na obiekcie proxy dla proxy HTTP. Brakujące albo złe Basic auth zwróciło HTTP 407 w Chromium 156.0.8078.4. Origin httpCredentials to nie dane logowania proxy.

Czy Chromium, Firefox i WebKit potrafią użyć SOCKS5 z nazwą użytkownika?

Playwright 1.64.0 odrzucił uwierzytelnianie SOCKS5 przy launch dla chromium, firefox i webkit komunikatem Browser does not support socks5 proxy authentication. Nieuwierzytelnione socks5:// zadziałało na Chromium w laboratorium.

Czy ustawić ignoreHTTPSErrors, żeby laboratoryjne CA przeszło?

Nie. Niezaufany lokalny TLS padł z net::ERR_CERT_AUTHORITY_INVALID po CONNECT. Node https z laboratoryjnym CA zweryfikował. ignoreHTTPSErrors w tym przewodniku zostaje false.

Czy bypass znaczy, że proxy nigdy nie jest używane?

Nie. bypass to lista hostów. 127.0.0.1 na liście bypass pominął proxy. Ten sam origin nadal użył proxy, gdy bypass było tylko example.com.

Czy sprawdzanie albo strona Wasze IP dowodzi trasy Playwright?

Nie. Sprawdzanie testuje, czy punkt końcowy odpowiada. Strona Wasze IP w przeglądarce to wyjście tej przeglądarki. Dowód Playwright to log CONNECT albo GET proxy plus wynik strony z procesu Playwright.