Proxy Playwright: configurazione del browser e del contesto

14 min

Impostare un proxy Playwright sul browser o su un contesto, passare le credenziali, confermare CONNECT su HTTPS e sistemare 407 ed errori di certificato senza spegnere il TLS.

Questa guida è per gli sviluppatori che devono far uscire il traffico di Playwright attraverso un proxy HTTP o SOCKS5, per l'intero browser o per un solo contesto. Il laboratorio ha usato Playwright 1.64.0 contro un'origine locale e un proxy fittizio in grado di fare CONNECT. Non ci sono chiavi gratuite, e Top Proxy non vende IP. Una fixture locale che passa non è un account o una regione di produzione.

01Impostare un proxy sul browser oppure su un contesto

Playwright documenta un unico oggetto proxy in due punti. Network: HTTP proxy (consultato il 14 ottobre 2026) dice che il proxy si può impostare in globale per l'intero browser, oppure per ciascun contesto. browser.newContext() ripete gli stessi campi: server, bypass opzionale, username opzionale, password opzionale. La guida di rete Python usa la stessa forma su chromium.launch(proxy=...) e browser.new_context(proxy=...). Questa procedura esegue la libreria Node. Python è simile a livello di API in quella documentazione, non è un secondo laboratorio.

Mettere il proxy su chromium.launch quando ogni contesto di quel processo deve condividere una sola uscita. Metterlo su browser.newContext quando un processo deve tenere due uscite, due jar di cookie, oppure un default più una sostituzione.

  1. Decidere se il lavoro è «una uscita per l'esecuzione» oppure «due identità in un processo».
  2. Se il lavoro è una uscita per l'intero processo del browser, impostare proxy su chromium.launch. In Playwright Test, use.proxy in playwright.config.ts è una fixture di contesto: si applica al contesto di quel test e alle sue pagine, non come default di avvio condiviso da ogni contesto del browser. Vedere Configuration (use).
  3. Se il lavoro è due uscite, avviare senza fissare la seconda identità, poi passare un proxy diverso in ciascun newContext.
  4. Chiudere ogni contesto creato prima di browser.close(), che è la nota di pulizia su browser.newContext().

Il laboratorio ha avviato Chromium 156.0.8078.4 con un proxy HTTP a livello di browser verso 127.0.0.1 e ha richiesto un'origine locale. La pagina ha restituito HTTP 200. Il log del proxy ha mostrato GET http://127.0.0.1:<port>/. Dopo aver rimosso HTTP_PROXY, HTTPS_PROXY, ALL_PROXY e NO_PROXY dal processo, un avvio diretto senza opzione proxy ha colpito la stessa origine con zero eventi proxy. È una baseline pulita senza proxy. Non misura che cosa farebbe Playwright se quelle variabili fossero ancora impostate.

Un secondo browser ha usato il primo proxy come default di avvio, poi ha aperto un contesto il cui proxy.server puntava a un altro forwarder locale. La richiesta del contesto ha restituito 200. I colpi sono arrivati sul secondo forwarder. Il primo forwarder non ha visto l'URL di origine del contesto. Il proxy del contesto ha sostituito il default del browser per quel contesto. Non si è fuso con esso.

use.proxy di Playwright Test configura il contesto del test e le sue pagine. chromium.launch({ proxy }) è un default di browser distinto. Non è un'API di rotazione per test. Non esiste un campo rotateSession su launch o newContext nell'API 1.64.0 che questo laboratorio ha eseguito.

02Passare host, porta e credenziali senza farle trapelare

server è un URL. Il testo dell'API Browser è esplicito: sono supportati http://myproxy.com:3128 e socks5://myproxy.com:3128. Una forma breve myproxy.com:3128 viene trattata come HTTP. Se il fornitore ha dato un endpoint SOCKS5, lo schema socks5:// è obbligatorio. Nome utente e password sono documentati come credenziali per un proxy HTTP che richiede autenticazione. Non sono documentati come userpass SOCKS5.

Tenere la password nell'ambiente o in un secret store. Non incollarla in una configurazione committata. Playwright deve comunque inviare Proxy-Authorization sul filo. Il laboratorio ha usato l'utente fittizio lab-user e una password fittizia che include @, :, /, # e %. I log dei risultati hanno redatto quel valore.

L'autenticazione HTTP del proxy in Chromium è loquace. Contro un proxy Basic locale:

  1. Impostare server su http://127.0.0.1:<port> senza nome utente. page.goto sull'origine HTTP ha restituito stato 407. Il log del proxy ha registrato authReason: missing. Playwright non ha lanciato un'eccezione. Lo stato sull'oggetto response era la prova.
  2. Impostare lo username lab-user e una password sbagliata. Di nuovo stato 407. Il log ha mostrato una prima sonda con credenziali mancanti, poi authReason: wrong.
  3. Impostare correttamente la password fittizia. Stato 200. Il log ha mostrato la stessa sonda mancante, poi authorized: true.

Le credenziali sbagliate non sono un socket appeso in questa build di Chromium. Sono un HTTP 407 dal proxy, esposto come stato di navigazione. Se un wrapper in seguito lancia un'eccezione, tenere il 407 e la classe dell'eccezione. Non collassare entrambi in «il proxy non funziona».

HTTP Authentication sulla stessa pagina Network è httpCredentials per l'origine, non proxy.username. Mescolarli è un errore comune: 401 dell'origine e 407 del proxy sono hop diversi.

03Confermare che il traffico HTTPS usa CONNECT attraverso un proxy HTTP

Lo schema dell'URL di destinazione e lo schema dell'URL del proxy sono manopole diverse. Un'origine https://example.com/ usa comunque un proxy HTTP su http://127.0.0.1:<port>. Il metodo del proxy è CONNECT, destinazione example.com:443. Il laboratorio ha registrato quel CONNECT, poi Chromium ha completato il TLS contro il certificato pubblico e page.goto ha restituito 200. È HTTPS attraverso un proxy HTTP con la verifica lasciata accesa.

Un'origine HTTPS locale firmata da una CA di laboratorio è l'altra metà. Chromium 156.0.8078.4, ignoreHTTPSErrors lasciato al default documentato false, ha navigato a https://127.0.0.1:<https-port>/ attraverso lo stesso proxy HTTP. Il proxy ha registrato CONNECT 127.0.0.1:<https-port>. Poi Playwright ha lanciato PlaywrightError con net::ERR_CERT_AUTHORITY_INVALID. CONNECT è avvenuto. Il TLS è comunque fallito. Il fatto che la destinazione fosse https:// non ha trasformato il proxy in un «https proxy», e il fatto che il proxy accettasse CONNECT non ha fatto fidare Chromium di una CA privata.

https.request di Node verso la stessa origine con rejectUnauthorized: true e senza CA è fallito con unable to verify the first certificate. Lo stesso client con il PEM della CA di laboratorio ha restituito HTTP 200. È il percorso di fiducia documentato di Node. L'opzione clientCertificates di Playwright, aggiunta in v1.46 su newContext, è un certificato client per un'origine nominata, non un modo per installare una CA privata. L'interruttore documentato di Playwright che salterebbe il fallimento è ignoreHTTPSErrors: true. Questo articolo non lo usa. Non impostare nemmeno rejectUnauthorized: false.

Se il lavoro è «dimostrare CONNECT», leggere il log del proxy per CONNECT host:port. Se il lavoro è «dimostrare che la pagina è caricata», serve comunque un certificato di cui Chromium si fida già, oppure una CA installata da un processo fuori da questa guida. Una pagina di controllo in un browser personale è l'uscita di quel browser, non il percorso di Playwright. Usare Il mio IP solo come aiuto sull'uscita del browser. Usare il controllo proxy per vedere se un host accetta HTTP o SOCKS5 prima di puntarci Playwright.

Tabella delle asserzioni dal laboratorio loopback di Playwright 1.64.0 del 14 ottobre 2026

La figura è una tabella di risultati di questa esecuzione, non una cover 16:9 e non uno screenshot del controllo.

04Usare SOCKS5 solo dove il motore lo supporta davvero

La guida Network dice che le pagine possono caricarsi su proxy HTTP(S) o SOCKSv5. Chromium 156.0.8078.4 con server: "socks5://127.0.0.1:<port>" e senza nome utente ha richiesto l'origine HTTP con stato 200. La fixture SOCKS ha registrato CONNECT 127.0.0.1:<origin-port>. SOCKS5 non autenticato è nella procedura per questo Chromium fissato.

SOCKS5 autenticato no. chromium.launch, firefox.launch e webkit.launch in Playwright 1.64.0 hanno tutti lanciato PlaywrightError: Browser does not support socks5 proxy authentication. L'eccezione è avvenuta all'avvio, prima di page.goto. Playwright 1.64.0 ha scaricato Firefox 157.0 e WebKit 27.2 per quegli avvii. Il messaggio coincide con la richiesta di prodotto aperta da tempo microsoft/playwright#10567, ancora Open con etichetta P3-collecting-feedback il 14 ottobre 2026. Un commento del maintainer del 2021 lì registra che Chromium e Firefox mancano di userpass SOCKS5 nei motori stessi.

Se un fornitore offre solo SOCKS5 con un nome utente, questa build fissata di Playwright non invierà quel nome utente. Usare un proxy HTTP con username e password, oppure un endpoint SOCKS5 non autenticato che il motore ha davvero chiamato in un test come quello sotto. Il confronto di protocollo sta in SOCKS5 e proxy HTTP. Python Requests è un client diverso e un articolo diverso: Proxy in Python Requests.

05Escludere host selezionati senza trattare bypass come una fuga universale

bypass è un elenco di domini separati da virgola. L'esempio dell'API è ".com, chromium.org, .domain.com". Non è un interruttore che spegne ogni host, e non è una funzione di privacy.

  1. Avviare Chromium con proxy.server che punta al forwarder aperto e bypass: "127.0.0.1".
  2. Navigare a http://127.0.0.1:<origin>/. La pagina ha restituito 200. Il log del proxy per quella finestra era vuoto. L'host elencato ha saltato il proxy.
  3. Avviare di nuovo con bypass: "example.com" e navigare alla stessa origine 127.0.0.1. Stato 200, e il proxy ha registrato GET http://127.0.0.1:<origin>/. Un host non elencato ha comunque usato il proxy.

Questa è l'intera promessa. bypass non significa «questo proxy non può vedermi in generale». Significa «questi host, in questo contesto, non vengono inviati a questo proxy». Le regole di localhost in Chromium possono sorprendere in altre build. Misurare il log del proxy. Non assumere.

browser.newContext() crea un jar che non condivide i cookie con altri contesti. Combinarlo con un proxy per contesto quando due test non devono far trapelare una sessione sull'uscita sbagliata.

  1. Avviare Chromium senza proxy a livello di browser.
  2. Aprire il contesto A con il proxy A, il contesto B con il proxy B.
  3. Navigare entrambi alla stessa origine che imposta un cookie lab.
  4. Leggere context.cookies().

Il laboratorio ha ottenuto il cookie t-origin-a-67a984 nel contesto A e t-origin-a-ff4919 nel contesto B. I jar non erano uguali. Entrambe le navigazioni hanno restituito 200. L'isolamento qui è l'isolamento dei contesti Playwright più due server proxy. Non è il marketing delle sessioni sticky del fornitore, e non è la pretesa che un pool residenziale terrà lo stesso IP.

Chiudere i contesti aperti. Un contesto perso tiene in vita cookie e credenziali del proxy finché il browser non muore.

07Diagnosticare connessioni rifiutate, 407 e fallimenti di certificato senza disabilitare il TLS

Quando Playwright «non funziona», nominare l'hop che è fallito.

DoveProtocolloChe cosa ha osservato il laboratorio 1.64.0Limite del motore
chromium.launch() direttonessunoorigine HTTP 200, zero eventi proxynessuno
proxy.server HTTP a livello di browserHTTP GET200, il log del proxy mostra GET in forma assolutanessuno
proxy di contesto che sovrascrive l'avvioHTTP GET200 sul proxy del contesto, non su quello di avvionessuno
Proxy HTTP, Basic mancante o sbagliatoHTTP 407stato 407 di page.goto, nessuna eccezionenessuno
Proxy HTTP, Basic correttoHTTP GET200 dopo la sonda di autenticazione mancante di Chromiumnessuno
Proxy HTTP, origine HTTPS con CA pubblicaCONNECT poi TLSCONNECT example.com:443, pagina 200nessuno
Proxy HTTP, origine HTTPS con CA di laboratorioCONNECT poi TLSCONNECT registrato, poi net::ERR_CERT_AUTHORITY_INVALIDnessuna API CA-file sul contesto
SOCKS5, senza nome utenteSOCKS CONNECTChromium 200, CONNECT della fixtureautenticazione non coinvolta
SOCKS5 più nome utenterifiutato all'avvioBrowser does not support socks5 proxy authentication su Chromium, Firefox, WebKitPlaywright 1.64.0 + motori
proxy.server http://127.0.0.1:1TCPnet::ERR_PROXY_CONNECTION_FAILEDnessuno
bypass: "127.0.0.1"skiporigine 200, zero eventi proxyl'elenco è esatto, non globale

Proxy rifiutato: Chromium si è comunque avviato. page.goto ha poi lanciato PlaywrightError con net::ERR_PROXY_CONNECTION_FAILED. Il timeout di navigazione di default nel laboratorio era 8000 ms. Il caso rifiutato è fallito prima di quel timeout. Tenere il nome della classe PlaywrightError e il codice net::. Non rinominarlo in un timeout generico.

Fallimento di certificato: tenere il TLS acceso. Il percorso CA locale che è riuscito era Node https con ca impostato. Il percorso browser Playwright che è riuscito era https://example.com/ attraverso CONNECT, che ha già una CA pubblica. Il percorso browser Playwright che è fallito era l'origine di laboratorio. ignoreHTTPSErrors resta false.

Come controllare un percorso, in ordine:

  1. Confermare che la porta del proxy accetta una connessione. Una porta rifiutata è ERR_PROXY_CONNECTION_FAILED, non una password sbagliata.
  2. Per i proxy HTTP, guardare 407 contro 200. Password mancanti e sbagliate erano 407 in questo Chromium.
  3. Per le destinazioni HTTPS, cercare CONNECT nel log del proxy, poi guardare il TLS. CONNECT più ERR_CERT_AUTHORITY_INVALID significa che il tunnel ha funzionato e il certificato no.
  4. Per SOCKS5 con un nome utente, aspettarsi un errore di avvio in 1.64.0 piuttosto che un errore di pagina.
  5. Solo allora guardare il body dell'origine.

Il laboratorio pubblico sotto è la fixture visibile al lettore. Possiede l'origine, il proxy CONNECT e il processo Chromium, e li chiude in finally, anche sui fallimenti di costruttore e di avvio. Salvarlo come 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);
  });
}

Il 14 ottobre 2026 quello script ha restituito ok: true, HTTP 200 attraverso il proxy, CONNECT verso la porta HTTPS locale, net::ERR_CERT_AUTHORITY_INVALID in Chromium, e HTTP 200 da Node con la CA di laboratorio. Solo credenziali fittizie. openssl deve essere nel PATH.

Se dopo che il client funziona serve un fornitore, le schede del catalogo di Bright Data, Oxylabs, SOAX e Decodo (Smartproxy) hanno restituito HTTP 200 il 14 ottobre 2026 e citano HTTP e SOCKS5. Quelle pagine non erano l'uscita del laboratorio. L'aderenza al catalogo è testo di protocollo pubblicato, non un login Playwright attraverso quel fornitore.

Bright Data — proxy residenziali e datacenter
Bright Data — proxy residenziali e datacenter4.41442

Bright Data è una grande rete proxy per la raccolta dati: IP residenziali, mobile, ISP e datacenter in circa 195 paesi. Il traffico residenziale si può pagare a consumo senza abbonamento, e gli indirizzi datacenter e ISP arrivano in pacchetti da 10 IP. L'accesso residenziale e mobile richiede la verifica dell'identità.

10 paesi
5,00 USD
per 1 GB
Oxylabs — proxy residenziali e datacenter
Oxylabs — proxy residenziali e datacenter4.0771

Oxylabs vende proxy residenziali, mobile, ISP e datacenter sotto un unico abbonamento mensile da $30. Pagamenti: carte, bonifico, AliPay, PayPal.

10 paesi
0,45 USD
per 1 GB
SOAX — proxy residenziali e mobile
SOAX — proxy residenziali e mobile3.8218

SOAX vende proxy residenziali e mobile a rotazione su un pool di crediti condiviso, con datacenter e ISP negli stessi piani. Il Builder parte da $200/mese + IVA (200 crediti); il traffico Tier 1 è $3/GB sul Builder.

85 paesi
3,00 USD
per 1 GB
Decodo (Smartproxy) — proxy residenziali e datacenter
Decodo (Smartproxy) — proxy residenziali e datacenter4.22118

Decodo (ex Smartproxy) vende proxy residenziali e datacenter. Residenziali: $4/GB senza piano mensile, o pacchetti mensili da $3.75/GB per 3 GB. Pagamenti: carte, PayPal, Alipay, Google/Apple Pay, crypto (con limiti).

83 paesi
0,38 USD
per 1 GB

Per un controllo preventivo di host, porta e protocollo prima di collegare Playwright, usare il controllo proxy. Per un passaggio più approfondito su «questo proxy fa trapelare qualcosa», vedere Come controllare se un proxy funziona.

Il proxy deve stare sul browser o su un contesto?

Un proxy a livello di browser copre ogni contesto di quel browser. Un proxy a livello di contesto si applica a un jar e può sostituire il default del browser. Il laboratorio ha visto un proxy di contesto inviare traffico a un secondo forwarder mentre il proxy di avvio restava quieto.

Playwright invia l'HTTPS attraverso un proxy HTTP?

Sì. L'URL del proxy resta http://host:port. Chromium emette poi CONNECT verso host:443. Il laboratorio ha registrato CONNECT example.com:443 e ha ricevuto HTTP 200 con la verifica TLS accesa.

Come si passa l'autenticazione del proxy?

Impostare username e password sull'oggetto proxy per i proxy HTTP. Basic auth mancante o sbagliata ha restituito HTTP 407 in Chromium 156.0.8078.4. Gli httpCredentials dell'origine non sono credenziali del proxy.

Chromium, Firefox e WebKit possono usare SOCKS5 con un nome utente?

Playwright 1.64.0 ha rifiutato l'autenticazione SOCKS5 all'avvio per chromium, firefox e webkit con Browser does not support socks5 proxy authentication. socks5:// non autenticato ha funzionato su Chromium nel laboratorio.

Si deve impostare ignoreHTTPSErrors per far passare una CA di laboratorio?

No. Il TLS locale non fidato è fallito con net::ERR_CERT_AUTHORITY_INVALID dopo CONNECT. Node https con la CA di laboratorio ha verificato. ignoreHTTPSErrors resta false in questa guida.

bypass significa che il proxy non viene mai usato?

No. bypass è un elenco di host. 127.0.0.1 elencato in bypass ha saltato il proxy. La stessa origine ha comunque usato il proxy quando bypass era solo example.com.

Un controllo o una pagina my-ip dimostra il percorso di Playwright?

No. Il controllo verifica se un endpoint risponde. Una pagina my-ip del browser è l'uscita di quel browser. La prova Playwright è il log CONNECT o GET del proxy più il risultato di pagina dal processo Playwright.