Proxy Playwright : configuration du navigateur et du contexte
Posez un proxy Playwright sur le navigateur ou un contexte, passez les identifiants, confirmez le CONNECT HTTPS, et corrigez les 407 et les erreurs de certificat sans couper le TLS.
Ce guide s'adresse aux développeurs qui ont besoin que le trafic Playwright sorte par un proxy HTTP ou SOCKS5, soit pour tout un navigateur, soit pour un seul contexte. Le laboratoire a utilisé Playwright 1.64.0 contre une origine locale et un proxy factice capable de CONNECT. Pas de clés gratuites, et Top Proxy ne vend pas d'IP. Un montage local qui réussit n'est pas un compte de production ni une région.
01Poser un proxy sur le navigateur ou sur un contexte
Playwright documente un objet proxy à deux endroits. Network: HTTP proxy (consulté le 14 octobre 2026) dit que le proxy peut être posé globalement pour tout le navigateur, ou pour chaque contexte de navigateur. browser.newContext() répète les mêmes champs : server, bypass optionnel, username optionnel, password optionnel. Le guide réseau Python utilise la même forme sur chromium.launch(proxy=...) et browser.new_context(proxy=...). Ce tutoriel exécute la bibliothèque Node. Python est d'API similaire dans cette documentation, ce n'est pas un second laboratoire.
Posez le proxy sur chromium.launch quand chaque contexte de ce processus doit partager une sortie. Posez-le sur browser.newContext quand un processus doit tenir deux sorties, deux pots de cookies, ou un défaut plus une surcharge.
- Décidez si la tâche est « une sortie pour l'exécution » ou « deux identités dans un processus ».
- Si la tâche est une sortie pour tout le processus navigateur, posez
proxysurchromium.launch. Dans Playwright Test,use.proxydansplaywright.config.tsest une fixture de contexte : il s'applique au contexte de ce test et à ses pages, pas comme un défaut de lancement partagé par chaque contexte du navigateur. Voir Configuration (use). - Si la tâche est deux sorties, lancez sans épingler la seconde identité, puis passez un
proxydifférent dans chaquenewContext. - Fermez chaque contexte que vous avez créé avant
browser.close(), qui est la note de nettoyage debrowser.newContext().
Le laboratoire a lancé Chromium 156.0.8078.4 avec un proxy HTTP au niveau navigateur vers 127.0.0.1 et a récupéré une origine locale. La page a renvoyé HTTP 200. Le journal du proxy a montré GET http://127.0.0.1:<port>/. Après avoir retiré HTTP_PROXY, HTTPS_PROXY, ALL_PROXY et NO_PROXY du processus, un lancement direct sans option proxy a touché la même origine avec zéro événement proxy. C'est un témoin propre sans proxy. Cela ne mesure pas ce que Playwright ferait si ces variables étaient encore posées.
Un second navigateur a utilisé le premier proxy comme défaut de lancement, puis a ouvert un contexte dont proxy.server pointait vers un autre relais local. La requête du contexte a renvoyé 200. Le trafic est arrivé sur le second relais. Le premier relais n'a pas vu l'URL d'origine du contexte. Le proxy de contexte a remplacé le défaut du navigateur pour ce contexte. Il ne s'est pas fusionné avec lui.
use.proxy de Playwright Test configure le contexte de test et ses pages. chromium.launch({ proxy }) est un défaut de navigateur séparé. Ce n'est pas une API de rotation par test. Il n'y a pas de champ rotateSession sur launch ou newContext dans l'API 1.64.0 que ce laboratoire a exécutée.
02Passer hôte, port et identifiants sans les divulguer
server est une URL. Le texte de l'API Browser est explicite : http://myproxy.com:3128 et socks5://myproxy.com:3128 sont pris en charge. Une forme courte myproxy.com:3128 est traitée comme HTTP. Si le fournisseur a donné un point d'accès SOCKS5, le schéma socks5:// est obligatoire. Identifiant et mot de passe sont documentés comme identifiants pour un proxy HTTP qui exige une authentification. Ils ne sont pas documentés comme userpass SOCKS5.
Gardez le mot de passe dans l'environnement ou un magasin de secrets. Ne le collez pas dans une config versionnée. Playwright doit quand même envoyer Proxy-Authorization sur le fil. Le laboratoire a utilisé l'utilisateur factice lab-user et un mot de passe factice qui inclut @, :, /, # et %. Les journaux de résultats ont masqué cette valeur.
L'auth proxy HTTP de Chromium est bavarde. Contre un proxy Basic local :
- Posez
serversurhttp://127.0.0.1:<port>sans identifiant.page.gotosur l'origine HTTP a renvoyé le statut 407. Le journal du proxy a enregistréauthReason: missing. Playwright n'a pas levé d'exception. Le statut sur l'objet réponse était la preuve. - Posez l'identifiant
lab-useret un mauvais mot de passe. Statut 407 encore. Le journal a montré une première sonde sans identifiants, puisauthReason: wrong. - Posez le mot de passe factice correctement. Statut 200. Le journal a montré la même sonde manquante, puis
authorized: true.
De mauvais identifiants ne sont pas une socket qui pend dans cette compilation Chromium. C'est un HTTP 407 du proxy, remonté comme statut de navigation. Si un wrapper lève ensuite une exception, gardez le 407 et la classe d'exception. Ne fusionnez pas les deux en « le proxy ne marche pas ».
HTTP Authentication sur la même page Network est httpCredentials pour l'origine, pas proxy.username. Les mélanger est une erreur fréquente : 401 d'origine et 407 de proxy sont des sauts différents.
03Confirmer que le trafic HTTPS utilise CONNECT à travers un proxy HTTP
Le schéma de l'URL cible et le schéma de l'URL du proxy sont des boutons différents. Une origine https://example.com/ utilise encore un proxy HTTP sur http://127.0.0.1:<port>. La méthode proxy est CONNECT, cible example.com:443. Le laboratoire a journalisé ce CONNECT, puis Chromium a terminé le TLS contre le certificat public et page.goto a renvoyé 200. C'est du HTTPS à travers un proxy HTTP avec la vérification laissée activée.
Une origine HTTPS locale signée par une AC de laboratoire est l'autre moitié. Chromium 156.0.8078.4, ignoreHTTPSErrors laissé au défaut documenté false, a navigué vers https://127.0.0.1:<https-port>/ à travers le même proxy HTTP. Le proxy a journalisé CONNECT 127.0.0.1:<https-port>. Puis Playwright a levé PlaywrightError avec net::ERR_CERT_AUTHORITY_INVALID. CONNECT s'est produit. Le TLS a quand même échoué. Que la cible soit https:// n'a pas transformé le proxy en « proxy https », et le fait que le proxy accepte CONNECT n'a pas fait confiance à Chromium une AC privée.
https.request de Node vers cette même origine avec rejectUnauthorized: true et sans AC a échoué avec unable to verify the first certificate. Le même client avec le PEM de l'AC du lab a renvoyé HTTP 200. C'est le chemin de confiance Node documenté. L'option Playwright clientCertificates, ajoutée en v1.46 sur newContext, est un certificat client pour une origine nommée, pas un moyen d'installer une AC privée. L'interrupteur Playwright documenté qui sauterait l'échec est ignoreHTTPSErrors: true. Cet article ne l'utilise pas. Ne posez pas non plus rejectUnauthorized: false.
Si la tâche est « prouver CONNECT », lisez le journal du proxy pour CONNECT host:port. Si la tâche est « prouver que la page s'est chargée », vous avez encore besoin d'un certificat auquel Chromium fait déjà confiance, ou d'une AC installée par un processus hors de ce guide. Une page de vérificateur dans un navigateur personnel est la sortie de ce navigateur, pas la route de Playwright. Utilisez Mon IP seulement comme aide de sortie navigateur. Utilisez le vérificateur de proxies pour voir si un hôte accepte HTTP ou SOCKS5 avant d'y pointer Playwright.

La figure est un tableau de résultats de cette exécution, pas une image de couverture 16:9 et pas une capture du vérificateur.
04N'utiliser SOCKS5 que là où le moteur le prend vraiment en charge
Le guide Network dit que les pages peuvent se charger via un proxy HTTP(S) ou SOCKSv5. Chromium 156.0.8078.4 avec server: "socks5://127.0.0.1:<port>" et sans identifiant a récupéré l'origine HTTP avec le statut 200. Le montage SOCKS a journalisé CONNECT 127.0.0.1:<origin-port>. SOCKS5 non authentifié est dans le tutoriel pour ce Chromium figé.
SOCKS5 authentifié ne l'est pas. chromium.launch, firefox.launch et webkit.launch dans Playwright 1.64.0 ont tous levé PlaywrightError : Browser does not support socks5 proxy authentication. L'exception s'est produite au lancement, avant page.goto. Playwright 1.64.0 a téléchargé Firefox 157.0 et WebKit 27.2 pour ces lancements. Le message correspond à la demande produit ouverte de longue date microsoft/playwright#10567, encore Open avec le libellé P3-collecting-feedback le 14 octobre 2026. Un commentaire de mainteneur de 2021 y enregistre que Chromium et Firefox n'ont pas de userpass SOCKS5 dans les moteurs eux-mêmes.
Si un fournisseur n'offre que du SOCKS5 avec un identifiant, cette compilation Playwright figée n'enverra pas cet identifiant. Utilisez un proxy HTTP avec username et password, ou un point d'accès SOCKS5 non authentifié que le moteur a réellement composé dans un test comme celui ci-dessous. La comparaison de protocoles vit dans SOCKS5 ou HTTP. Python Requests est un client différent et un article différent : Proxy Python requests.
05Contourner des hôtes choisis sans traiter bypass comme une échappatoire universelle
bypass est une liste de domaines séparés par des virgules. L'exemple d'API est ".com, chromium.org, .domain.com". Ce n'est pas un interrupteur général pour chaque hôte, et ce n'est pas une fonctionnalité de confidentialité.
- Lancez Chromium avec
proxy.serverpointé vers le relais ouvert etbypass: "127.0.0.1". - Naviguez vers
http://127.0.0.1:<origin>/. La page a renvoyé 200. Le journal du proxy pour cette fenêtre était vide. L'hôte listé a sauté le proxy. - Relancez avec
bypass: "example.com"et naviguez vers la même origine127.0.0.1. Statut 200, et le proxy a journaliséGET http://127.0.0.1:<origin>/. Un hôte non listé a encore utilisé le proxy.
C'est toute la promesse. bypass ne veut pas dire « ce proxy ne peut pas me voir en général ». Cela veut dire « ces hôtes, dans ce contexte, ne sont pas envoyés à ce proxy ». Les règles localhost de Chromium peuvent surprendre dans d'autres compilations. Mesurez le journal du proxy. Ne supposez pas.
06Isoler deux contextes pour que cookies et sorties ne se mélangent pas
browser.newContext() crée un pot qui ne partage pas les cookies avec les autres contextes. Combinez cela avec un proxy par contexte quand deux tests ne doivent pas fuiter une session sur la mauvaise sortie.
- Lancez Chromium sans proxy au niveau navigateur.
- Ouvrez le contexte A avec le proxy A, le contexte B avec le proxy B.
- Naviguez les deux vers la même origine qui pose un cookie
lab. - Lisez
context.cookies().
Le laboratoire a obtenu le cookie t-origin-a-67a984 dans le contexte A et t-origin-a-ff4919 dans le contexte B. Les pots n'étaient pas égaux. Les deux navigations ont renvoyé 200. L'isolation ici est l'isolation de contexte Playwright plus deux serveurs proxy. Ce n'est pas du marketing sticky-session de fournisseur, et ce n'est pas l'affirmation qu'un pool résidentiel gardera la même IP.
Fermez les contextes que vous avez ouverts. Un contexte qui fuit garde cookies et identifiants proxy en vie jusqu'à la mort du navigateur.
07Diagnostiquer connexions refusées, 407 et échecs de certificat sans désactiver le TLS
Quand Playwright « ne marche pas », nommez le saut qui a échoué.
| Où | Protocole | Ce que le lab 1.64.0 a observé | Limite du moteur |
|---|---|---|---|
chromium.launch() direct | aucun | Origine HTTP 200, zéro événement proxy | aucune |
proxy.server HTTP au niveau navigateur | HTTP GET | 200, le journal du proxy montre un GET en forme absolue | aucune |
proxy de contexte qui surcharge le lancement | HTTP GET | 200 sur le proxy du contexte, pas le proxy de lancement | aucune |
| Proxy HTTP, Basic manquant ou faux | HTTP 407 | statut page.goto 407, pas d'exception | aucune |
| Proxy HTTP, Basic correct | HTTP GET | 200 après la sonde sans auth de Chromium | aucune |
| Proxy HTTP, origine HTTPS avec AC publique | CONNECT puis TLS | CONNECT example.com:443, page 200 | aucune |
| Proxy HTTP, origine HTTPS avec AC de lab | CONNECT puis TLS | CONNECT journalisé, puis net::ERR_CERT_AUTHORITY_INVALID | pas d'API de fichier d'AC de contexte |
| SOCKS5, pas d'identifiant | SOCKS CONNECT | Chromium 200, CONNECT du montage | auth non impliquée |
| SOCKS5 plus identifiant | rejeté au lancement | Browser does not support socks5 proxy authentication sur Chromium, Firefox, WebKit | Playwright 1.64.0 + moteurs |
proxy.server http://127.0.0.1:1 | TCP | net::ERR_PROXY_CONNECTION_FAILED | aucune |
bypass: "127.0.0.1" | skip | origine 200, zéro événement proxy | la liste est exacte, pas globale |
Proxy refusé : Chromium s'est quand même lancé. page.goto a ensuite levé PlaywrightError avec net::ERR_PROXY_CONNECTION_FAILED. Le délai de navigation par défaut dans le laboratoire était de 8000 ms. Le cas refusé a échoué plus vite que ce délai. Gardez le nom de classe PlaywrightError et le code net::. Ne le renommez pas en délai générique.
Échec de certificat : gardez le TLS activé. Le chemin d'AC locale qui a réussi était Node https avec ca posé. Le chemin navigateur Playwright qui a réussi était https://example.com/ à travers CONNECT, qui a déjà une AC publique. Le chemin navigateur Playwright qui a échoué était l'origine du lab. ignoreHTTPSErrors reste false.
Comment vérifier une route, dans l'ordre :
- Confirmez que le port du proxy accepte une connexion. Un port refusé est
ERR_PROXY_CONNECTION_FAILED, pas un mauvais mot de passe. - Pour les proxies HTTP, regardez 407 contre 200. Identifiants manquants et faux étaient 407 dans ce Chromium.
- Pour les cibles HTTPS, cherchez CONNECT dans le journal du proxy, puis regardez le TLS. CONNECT plus
ERR_CERT_AUTHORITY_INVALIDveut dire que le tunnel a marché et que le certificat n'a pas marché. - Pour SOCKS5 avec un identifiant, attendez-vous à une erreur de lancement en 1.64.0 plutôt qu'à une erreur de page.
- Seulement ensuite, regardez le corps de l'origine.
Le laboratoire public ci-dessous est le montage visible du lecteur. Il possède l'origine, le proxy CONNECT et le processus Chromium, et il les ferme dans finally, y compris sur les échecs de constructeur et de démarrage. Enregistrez-le sous 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);
});
}
Le 14 octobre 2026, ce script a renvoyé ok: true, HTTP 200 à travers le proxy, CONNECT vers le port HTTPS local, net::ERR_CERT_AUTHORITY_INVALID dans Chromium, et HTTP 200 depuis Node avec l'AC du lab. Identifiants factices seulement. openssl doit être dans le PATH.
Si vous avez besoin d'un fournisseur une fois que le client marche, les fiches du catalogue de Bright Data, Oxylabs, SOAX et Decodo (Smartproxy) ont renvoyé HTTP 200 le 14 octobre 2026 et mentionnent HTTP et SOCKS5. Ces pages n'étaient pas la sortie du laboratoire. L'adéquation au catalogue est le texte de protocole publié, pas un login Playwright à travers ce fournisseur.

Bright Data est un grand réseau de proxies pour la collecte de données : IP résidentielles, mobiles, ISP et datacenter dans environ 195 pays. Le trafic résidentiel peut se payer à l'usage sans abonnement, et les adresses datacenter et ISP se vendent par packs dès 10 IP. L'accès résidentiel et mobile exige une vérification d'identité.

Oxylabs vend des proxies résidentiels, mobiles, ISP et datacenter sous un seul abonnement mensuel dès $30. Paiements acheteur : cartes, virement, AliPay, PayPal.

SOAX vend des proxies résidentiels et mobiles rotatifs sur un pool de crédits partagé, avec datacenter et ISP dans les mêmes forfaits. Builder commence à $200/mois + TVA (200 crédits) ; le trafic Tier 1 est à $3/GB sur Builder.

Decodo (anciennement Smartproxy) vend des proxies résidentiels et datacenter. Résidentiel : $4/GB sans forfait mensuel, ou packs mensuels dès $3.75/GB pour 3 GB. Paiements : cartes, PayPal, Alipay, Google/Apple Pay, crypto (avec limites).
Pour un pré-contrôle d'hôte, de port et de protocole avant de brancher Playwright, utilisez le vérificateur de proxies. Pour un passage plus profond « ce proxy fuit-il », voir Comment vérifier qu'un proxy fonctionne.
Le proxy doit-il être sur le navigateur ou sur un contexte ?
Playwright envoie-t-il le HTTPS à travers un proxy HTTP ?
Comment passer l'authentification du proxy ?
Chromium, Firefox et WebKit peuvent-ils utiliser SOCKS5 avec un identifiant ?
Faut-il poser ignoreHTTPSErrors pour faire passer une AC de laboratoire ?
bypass veut-il dire que le proxy n'est jamais utilisé ?
Un vérificateur ou une page Mon IP prouve-t-il la route Playwright ?
Articles associés
Qu'est-ce qu'un proxy résidentiel et comment ça marche
Les proxies résidentiels sortent par des adresses d'opérateur grand public. Pools, sessions sticky, ce que le site voit, et quoi demander au fournisseur avant d'acheter.
4 min de lectureMeilleurs proxies pour le web scraping en 2026 : une liste de contrôle d'achat
Un achat de scraping en 2026 commence par la cible, pas par un fournisseur. Utilisez cette liste pour l'origine, la session, la géo, la facturation, et un test en direct avant de monter.
5 min de lectureProxies sticky ou rotatifs : le contrôle de session expliqué
Proxies sticky ou rotatifs : rotation à chaque requête ou sur minuterie, quand une session sticky bat une nouvelle IP, comment les cookies réagissent à un changement d'IP, et comment tester un identifiant de session.
8 min de lecture

