Proxy w Playwright: konfiguracja przeglądarki i kontekstu
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.
- Zdecydować, czy zadanie to „jedno wyjście na cały przebieg”, czy „dwie tożsamości w jednym procesie”.
- Jeśli zadanie to jedno wyjście dla całego procesu przeglądarki, ustawić
proxynachromium.launch. W Playwright Testuse.proxywplaywright.config.tsjest 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). - Jeśli zadanie to dwa wyjścia, uruchomić bez przypinania drugiej tożsamości, a potem przekazać inne
proxydo każdegonewContext. - Zamknąć każdy utworzony kontekst przed
browser.close(), co jest uwagą o sprzątaniu przybrowser.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:
- Ustawić
servernahttp://127.0.0.1:<port>bez nazwy użytkownika.page.gotona origin HTTP zwróciło status 407. Log proxy zapisałauthReason: missing. Playwright nie rzucił. Status na obiekcie odpowiedzi był dowodem. - Ustawić użytkownika
lab-useri złe hasło. Znowu status 407. Log pokazał pierwszą sondę bez danych, potemauthReason: wrong. - 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ć.

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.
- Uruchomić Chromium z
proxy.serverwskazującym otwarty forwarder ibypass: "127.0.0.1". - Nawigować do
http://127.0.0.1:<origin>/. Strona zwróciła 200. Log proxy dla tego okna był pusty. Wymieniony host pominął proxy. - Uruchomić znowu z
bypass: "example.com"i nawigować do tego samego origin127.0.0.1. Status 200, a proxy zapisałoGET 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.
- Uruchomić Chromium bez proxy na poziomie przeglądarki.
- Otworzyć kontekst A z proxy A, kontekst B z proxy B.
- Nawigować oba do tego samego origin, który ustawia ciasteczko
lab. - 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ł.
| Gdzie | Protokół | Co zaobserwowało laboratorium 1.64.0 | Limit silnika |
|---|---|---|---|
Bezpośredni chromium.launch() | brak | origin HTTP 200, zero zdarzeń proxy | brak |
proxy.server HTTP na poziomie przeglądarki | HTTP GET | 200, log proxy pokazuje GET w formie absolutnej | brak |
proxy kontekstu nadpisujące launch | HTTP GET | 200 na proxy kontekstu, nie na proxy launch | brak |
| Proxy HTTP, brakujące albo złe Basic | HTTP 407 | status page.goto 407, bez rzutu | brak |
| Proxy HTTP, poprawne Basic | HTTP GET | 200 po sondzie Chromium bez danych | brak |
| Proxy HTTP, origin HTTPS z publicznym CA | CONNECT, potem TLS | CONNECT example.com:443, strona 200 | brak |
| Proxy HTTP, origin HTTPS z laboratoryjnym CA | CONNECT, potem TLS | CONNECT zapisany, potem net::ERR_CERT_AUTHORITY_INVALID | brak API pliku CA na kontekście |
| SOCKS5, bez nazwy użytkownika | SOCKS CONNECT | Chromium 200, CONNECT atrapy | auth nie bierze udziału |
| SOCKS5 plus nazwa użytkownika | odrzucone przy launch | Browser does not support socks5 proxy authentication na Chromium, Firefox, WebKit | Playwright 1.64.0 + silniki |
proxy.server http://127.0.0.1:1 | TCP | net::ERR_PROXY_CONNECTION_FAILED | brak |
bypass: "127.0.0.1" | pominięcie | origin 200, zero zdarzeń proxy | lista 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:
- Potwierdzić, że port proxy przyjmuje połączenie. Odmówiony port to
ERR_PROXY_CONNECTION_FAILED, nie złe hasło. - Dla proxy HTTP patrzeć na 407 kontra 200. Brakujące i złe hasła były 407 w tym Chromium.
- Dla celów HTTPS szukać CONNECT w logu proxy, a potem patrzeć na TLS. CONNECT plus
ERR_CERT_AUTHORITY_INVALIDznaczy, że tunel działał, a certyfikat nie. - Dla SOCKS5 z nazwą użytkownika oczekiwać błędu launch w 1.64.0 raczej niż błędu strony.
- 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 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.

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

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.

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).
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?
Czy Playwright wysyła HTTPS przez proxy HTTP?
Jak przekazać uwierzytelnianie proxy?
Czy Chromium, Firefox i WebKit potrafią użyć SOCKS5 z nazwą użytkownika?
Czy ustawić ignoreHTTPSErrors, żeby laboratoryjne CA przeszło?
Czy bypass znaczy, że proxy nigdy nie jest używane?
Czy sprawdzanie albo strona Wasze IP dowodzi trasy Playwright?
Powiązane artykuły
Czym są proxy rezydenckie i jak działają
Proxy rezydenckie wychodzą przez adresy konsumenckich ISP. Tu jest, jak działają pule, sesje sticky, co widzą witryny i o co pytać dostawcę przed zakupem.
4 minNajlepsze proxy do web scrapingu w 2026: lista kontrolna kupującego
Zakup proxy do scrapingu w 2026 zaczyna się od celu, nie od sprzedawcy. Ta lista kontrolna obejmuje pochodzenie, sesję, geo, rozliczenie i żywy test, zanim przeskalujecie.
5 minSticky a rotacyjne proxy: sterowanie sesją wyjaśnione
Sticky a rotacyjne proxy: jak działa rotacja na żądanie i na timerze, kiedy sesja sticky bije nowy IP, jak ciasteczka reagują na zmianę adresu i jak samodzielnie przetestować identyfikator sesji.
9 min

