Playwrightのプロキシ: ブラウザとコンテキストの設定
Playwrightのプロキシをブラウザまたはコンテキストに設定し、資格情報を渡し、HTTPSのCONNECTを確認し、TLSを切らずに407と証明書の失敗を直します。
Playwrightのトラフィックを、ブラウザ全体または一つのコンテキストから、HTTPまたはSOCKS5プロキシ経由で出す必要がある開発者向けの案内です。実験はPlaywright 1.64.0を、ローカルオリジンとCONNECTできるダミープロキシに当てました。無料の鍵はありません。Top ProxyはIPを売りません。通ったローカルフィクスチャは、本番の口座でも地域でもありません。
01ブラウザのプロキシとコンテキストのプロキシ
Playwrightは、二つの場所に一つの proxy オブジェクトを文書化しています。Network: HTTP proxy(2026年10月14日に参照)は、プロキシをブラウザ全体のグローバル、またはブラウザコンテキストごとに置けると書いています。browser.newContext()も同じ欄を繰り返します。server、任意の bypass、任意の username、任意の password です。Pythonのネットワーク案内も、chromium.launch(proxy=...) と browser.new_context(proxy=...) で同じ形です。この手順が実行するのはNodeのライブラリです。Pythonはそれらの文書でAPIが近いだけであり、第二の実験ではありません。
そのプロセスのすべてのコンテキストが一つの出口を共有するなら、プロキシは chromium.launch へ置いてください。一つのプロセスが出口を二つ、Cookieの壺を二つ、または既定と上書きを持つ必要があるなら、browser.newContext へ置いてください。
- 仕事が「実行全体で一つの出口」か「一つのプロセスに二つの身元」かを決めてください。
- 仕事がブラウザプロセス全体で一つの出口なら、
proxyをchromium.launchへ置きます。Playwright Testでは、playwright.config.tsのuse.proxyはコンテキストのフィクスチャです。その試験のコンテキストとそのページに効き、ブラウザ上のすべてのコンテキストが共有する起動の既定ではありません。Configuration (use)を見てください。 - 仕事が出口二つなら、二番目の身元を固定せずに起動し、各
newContextへ別のproxyを渡してください。 - 作ったコンテキストは、
browser.close()の前にそれぞれ閉じてください。browser.newContext()の後始末の注記です。
実験はChromium 156.0.8078.4を、127.0.0.1 へのブラウザ級HTTPプロキシ付きで起動し、ローカルオリジンを取りました。ページはHTTP 200を返しました。プロキシのログは GET http://127.0.0.1:<port>/ を示しました。プロセスから HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY を外したあと、proxy オプションなしの直接起動は、同じオリジンにプロキシイベント0で当たりました。きれいなプロキシなしの基準です。それらの変数が残っていたときにPlaywrightが何をするかは測っていません。
二番目のブラウザは、起動の既定に最初のプロキシを使い、そのあと proxy.server が別のローカル転送器を指すコンテキストを開きました。コンテキストの要求は200を返しました。ヒットは二番目の転送器に落ちました。最初の転送器は、コンテキストのオリジンURLを見ませんでした。コンテキストの proxy は、そのコンテキストのブラウザ既定を置き換えます。マージはしません。
Playwright Testの use.proxy は、試験コンテキストとそのページを設定します。chromium.launch({ proxy }) は別のブラウザ既定です。試験ごとのローテーションAPIではありません。この実験が実行した1.64.0のAPIに、launch や newContext の rotateSession 欄はありません。
02ホスト、ポート、資格情報を漏さずに渡す
server はURLです。Browser APIの文は明確です。http://myproxy.com:3128 と socks5://myproxy.com:3128 に対応します。短い形 myproxy.com:3128 はHTTPとして扱われます。供給元がSOCKS5端点を渡したなら、socks5:// スキームが必要です。ユーザー名とパスワードは、認証が必要なHTTPプロキシの資格情報として文書化されています。SOCKS5のuserpassとしては文書化されていません。
パスワードは環境またはシークレットストアに置いてください。コミットする設定へ貼り付けないでください。Playwrightは、それでも回線へ Proxy-Authorization を送る必要があります。実験はダミーユーザー lab-user と、@、:、/、#、% を含むダミーパスワードを使いました。結果のログはその値を伏せています。
ChromiumのHTTPプロキシ認証はおしゃべりです。ローカルのBasicプロキシに対して:
serverをhttp://127.0.0.1:<port>にし、ユーザー名なし。HTTPオリジンへのpage.gotoはステータス407を返しました。プロキシのログはauthReason: missingを記録しました。Playwrightは投げませんでした。証明は応答オブジェクトのステータスです。- ユーザー名
lab-userと間違ったパスワード。またステータス407。ログは、資格情報なしの最初の探り、そのあとauthReason: wrongを示しました。 - ダミーパスワードを正しく設定。ステータス200。ログは同じ探りのあと、
authorized: trueを示しました。
このChromiumビルドでは、間違った資格情報は吊るソケットではありません。プロキシからのHTTP 407であり、案内のステータスとして出ます。ラッパーがあとで投げるなら、407と例外クラスを残してください。両方を「プロキシが動かない」へ潰さないでください。
同じNetworkページのHTTP Authenticationは、オリジン向けの httpCredentials であり、proxy.username ではありません。混ぜるのはよくある取り違えです。オリジンの401とプロキシの407は、別のホップです。
03HTTPSトラフィックがHTTPプロキシでCONNECTを使うことを確認する
対象URLのスキームとプロキシURLのスキームは、別のつまみです。オリジン https://example.com/ でも、プロキシは http://127.0.0.1:<port> のHTTPプロキシのままです。プロキシのメソッドは CONNECT、対象は example.com:443 です。実験はそのCONNECTを記録し、Chromiumは公開証明書に対してTLSを終え、page.goto は200を返しました。検証を付けたまま、HTTPプロキシ越しのHTTPSです。
実験CAで署名したローカルHTTPSオリジンが、もう半分です。Chromium 156.0.8078.4、ignoreHTTPSErrors は文書の既定 false のまま、同じHTTPプロキシ越しに https://127.0.0.1:<https-port>/ へ案内しました。プロキシは CONNECT 127.0.0.1:<https-port> を記録しました。そのあとPlaywrightは PlaywrightError を投げ、中身は net::ERR_CERT_AUTHORITY_INVALID でした。CONNECTは起きました。TLSはまだ失敗しました。対象が https:// だからといって、プロキシが「httpsプロキシ」になるわけではありません。プロキシがCONNECTを受けても、Chromiumが私的CAを信頼するわけでもありません。
同じオリジンへのNode https.request は、rejectUnauthorized: true でCAなしだと unable to verify the first certificate で失敗しました。同じクライアントに実験CAのPEMを付けるとHTTP 200が返りました。それが文書どおりのNodeの信頼経路です。Playwrightの clientCertificates オプションは、v1.46で newContext に足された、指名したオリジン向けのクライアント証明書であり、私的CAを入れる方法ではありません。失敗を飛ばすと文書化されたPlaywrightのスイッチは ignoreHTTPSErrors: true です。この記事では使いません。rejectUnauthorized: false も付けないでください。
仕事が「CONNECTを証明する」なら、プロキシのログで CONNECT host:port を読んでください。仕事が「ページが読み込まれたことを証明する」なら、Chromiumがすでに信頼する証明書、またはこの案内の外のプロセスが入れたCAが、まだ必要です。個人ブラウザのチェッカーページは、そのブラウザの出口であり、Playwrightの経路ではありません。自分のIPは、ブラウザ出口の補助としてだけ使ってください。Playwrightを向ける前に、ホストがHTTPまたはSOCKS5を受けるかはプロキシチェッカーで見てください。

図はこの実行の結果表です。16:9の表紙ではなく、チェッカーのスクリーンショットでもありません。
04エンジンが実際に対応する場所だけSOCKS5を使う
Network案内は、ページはHTTP(S)プロキシまたはSOCKSv5で読み込めると書いています。Chromium 156.0.8078.4は、server: "socks5://127.0.0.1:<port>" でユーザー名なしだと、HTTPオリジンをステータス200で取りました。SOCKSフィクスチャは CONNECT 127.0.0.1:<origin-port> を記録しました。認証なしのSOCKS5は、この固定Chromiumの手順に入っています。
認証付きSOCKS5は入っていません。Playwright 1.64.0の chromium.launch、firefox.launch、webkit.launch はすべて PlaywrightError を投げました。Browser does not support socks5 proxy authentication です。投げは起動時で、page.goto の前です。Playwright 1.64.0は、それらの起動のためにFirefox 157.0とWebKit 27.2をダウンロードしました。メッセージは、長く開いた製品要求 microsoft/playwright#10567 と一致します。2026年10月14日時点でもOpenで、ラベルは P3-collecting-feedback です。2021年のメンテナコメントは、ChromiumとFirefoxのエンジン自身にSOCKS5 userpassが無いと記録しています。
供給元がユーザー名付きSOCKS5しか出さないなら、この固定Playwrightビルドはそのユーザー名を送りません。username と password 付きのHTTPプロキシか、下のような試験でエンジンが実際にダイヤルした認証なしSOCKS5端点を使ってください。プロトコル比較はSOCKS5とHTTPプロキシにあります。PythonのRequestsは別のクライアント、別の記事です。Python requestsのプロキシ。
05バイパスを万能の逃げ道にせず、選んだホストだけ飛ばす
bypass はドメインのカンマ区切り一覧です。APIの例は ".com, chromium.org, .domain.com" です。すべてのホストのキルスイッチではありません。プライバシー機能でもありません。
proxy.serverを開いた転送器へ向け、bypass: "127.0.0.1"でChromiumを起動します。http://127.0.0.1:<origin>/へ案内します。ページは200を返しました。その窓のプロキシログは空でした。一覧のホストはプロキシを飛ばしました。bypass: "example.com"で再起動し、同じ127.0.0.1オリジンへ案内します。ステータス200、プロキシはGET http://127.0.0.1:<origin>/を記録しました。一覧に無いホストは、まだプロキシを使いました。
約束はそれだけです。bypass は「このプロキシは全般に私を見られない」ではありません。「これらのホストは、このコンテキストでは、このプロキシへ送られない」です。Chromiumのlocalhost規則は、別ビルドでは驚くことがあります。プロキシのログを測ってください。仮定しないでください。
06二つのコンテキストを隔離し、Cookieと出口を混ぜない
browser.newContext() は、ほかのコンテキストとCookieを共有しない壺を作ります。二つの試験がセッションを間違った出口へ漏らしてはいけないとき、コンテキストごとの proxy と組み合わせてください。
- ブラウザ級プロキシなしでChromiumを起動します。
- コンテキストAをプロキシA、コンテキストBをプロキシBで開きます。
- 両方を、
labCookieを付ける同じオリジンへ案内します。 context.cookies()を読みます。
実験は、コンテキストAでCookie t-origin-a-67a984、コンテキストBで t-origin-a-ff4919 を得ました。壺は等しくありませんでした。両方の案内は200を返しました。ここでの隔離は、Playwrightのコンテキスト隔離にプロキシサーバー二つです。供給元のスティッキーセッション宣伝ではありません。レジデンシャルプールが同じIPを保つという主張でもありません。
開いたコンテキストは閉じてください。漏れたコンテキストは、ブラウザが死ぬまでCookieとプロキシ資格情報を生きたままにします。
07拒否された接続、407、証明書の失敗を、TLSを切らずに診断する
Playwrightが「動かない」ときは、失敗したホップに名前を付けてください。
| 場所 | プロトコル | 1.64.0の実験が観測したこと | エンジンの制限 |
|---|---|---|---|
直接の chromium.launch() | なし | HTTPオリジン200、プロキシイベント0 | なし |
ブラウザ級 proxy.server HTTP | HTTP GET | 200、プロキシログはabsolute-form GET | なし |
起動を上書きするコンテキスト proxy | HTTP GET | コンテキストのプロキシで200。起動プロキシではない | なし |
| HTTPプロキシ、無いまたは間違ったBasic | HTTP 407 | page.goto のステータス407、投げなし | なし |
| HTTPプロキシ、正しいBasic | HTTP GET | Chromiumの認証なし探りのあと200 | なし |
| HTTPプロキシ、公開CAのHTTPSオリジン | CONNECTのあとTLS | CONNECT example.com:443、ページ200 | なし |
| HTTPプロキシ、実験CAのHTTPSオリジン | CONNECTのあとTLS | CONNECT記録、そのあと net::ERR_CERT_AUTHORITY_INVALID | コンテキストのCAファイルAPIなし |
| SOCKS5、ユーザー名なし | SOCKS CONNECT | Chromium 200、フィクスチャCONNECT | 認証は関与せず |
| SOCKS5とユーザー名 | 起動時に拒否 | Chromium、Firefox、WebKitで Browser does not support socks5 proxy authentication | Playwright 1.64.0とエンジン |
proxy.server http://127.0.0.1:1 | TCP | net::ERR_PROXY_CONNECTION_FAILED | なし |
bypass: "127.0.0.1" | 飛ばす | オリジン200、プロキシイベント0 | 一覧は正確。全体ではない |
拒否されたプロキシ: Chromiumは起動しました。そのあと page.goto が PlaywrightError を投げ、中身は net::ERR_PROXY_CONNECTION_FAILED でした。実験の既定案内タイムアウトは8000 msでした。拒否のケースはそのタイムアウトより早く失敗しました。クラス名 PlaywrightError と net:: コードを残してください。一般のタイムアウトへ名前を変えないでください。
証明書の失敗: TLSは付けたままです。成功したローカルCA経路は、ca を付けたNode https でした。成功したPlaywrightブラウザ経路は、CONNECT越しの https://example.com/ で、すでに公開CAがあります。失敗したPlaywrightブラウザ経路は実験オリジンでした。ignoreHTTPSErrors はfalseのままです。
経路の確認順です。
- プロキシのポートが接続を受けることを確認してください。拒否されたポートは
ERR_PROXY_CONNECTION_FAILEDであり、悪いパスワードではありません。 - HTTPプロキシでは、407対200を見てください。このChromiumでは、無いパスワードも間違ったパスワードも407でした。
- HTTPS対象では、プロキシログのCONNECTを見て、そのあとTLSを見てください。CONNECTに
ERR_CERT_AUTHORITY_INVALIDが続くなら、トンネルは動き、証明書は動きませんでした。 - ユーザー名付きSOCKS5では、1.64.0ではページエラーではなく起動エラーを想定してください。
- そのあとだけ、オリジンの本文を見てください。
下の公開実験は、読者に見えるフィクスチャです。オリジン、CONNECTプロキシ、Chromiumプロセスを持ち、コンストラクタと起動の失敗も含め、finally で閉じます。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);
});
}
2026年10月14日、そのスクリプトは ok: true、プロキシ越しのHTTP 200、ローカルHTTPSポートへの CONNECT、Chromiumの net::ERR_CERT_AUTHORITY_INVALID、実験CA付きNodeからのHTTP 200を返しました。ダミー資格情報だけです。opensslは PATH にある必要があります。
クライアントが動いたあとに供給元が要るなら、Bright Data、Oxylabs、SOAX、Decodo (Smartproxy)のカタログページは、2026年10月14日にHTTP 200を返し、HTTPとSOCKS5に触れています。それらのページは実験の出口ではありませんでした。カタログの適合は公開されたプロトコルの文であり、その業者へのPlaywrightログインではありません。

Bright Dataはデータ収集向けの大規模プロキシ網です。レジデンシャル、モバイル、ISP、データセンターのIPが約195か国にあります。レジデンシャルトラフィックは定期契約なしの従量払いができ、データセンターとISPアドレスは10 IPからのパックです。レジデンシャルとモバイルの利用には本人確認が必要です。

Oxylabsはレジデンシャル、モバイル、ISP、データセンタープロキシを、月$30からのひとつの月次契約で売っています。支払いはカード、電信送金、AliPay、PayPalです。
Playwrightを配線する前のホスト、ポート、プロトコルの事前確認には、プロキシチェッカーを使ってください。より深い「このプロキシは漏らすか」の通過は、プロキシが動くかの確認を見てください。
プロキシはブラウザとコンテキストのどちらに置くべきですか?
PlaywrightはHTTPプロキシ越しにHTTPSを送りますか?
プロキシ認証はどう渡しますか?
Chromium、Firefox、WebKitはユーザー名付きSOCKS5を使えますか?
実験CAを通すためにignoreHTTPSErrorsを付けるべきですか?
bypassはプロキシを一度も使わない意味ですか?
チェッカーや自分のIPのページは、Playwrightの経路を証明しますか?
関連記事
レジデンシャルプロキシとは何か、どう動くか
レジデンシャルプロキシは一般向けISPのアドレスから出ます。プール、スティッキーセッション、サイトから見えるもの、購入前に供給元へ聞くことを説明します。
4分2026年のスクレイピング向けプロキシ: 購入チェックリスト
2026年のスクレイピング購入は、供給元ではなく相手から始めます。出自、セッション、地理、課金、本番前の実地試験のチェックリストです。
5分スティッキーとローテーション: セッション制御の仕組み
スティッキーとローテーション: リクエストごととタイマーのローテーションの動き、スティッキーが新しいIPに勝つとき、CookieとIP変更の関係、セッションIDの自分での試験。
8分



