[Bug]: Uncaught TypeError in captureSecurityDetails when APIRequestContext reuses a destroyed keep-alive socket
メンテナーはふだん 1 日以内に返信
@microsoft-playwright-automation がすでに取り組んでいます。
2026年10月28日 から。
- #43729 @microsoft-playwright-automation による — オープン
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 76/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- node.js, typescript
- 領域
- backend, networking
調査の方向性
packages/playwright-core/src/server/fetch.tsで、報告されたcaptureSecurityDetailsの呼び出しと証明書の読み取り箇所から始めます。issueにある自己完結型のNodeスクリプトで再現し、その後、破棄された再利用TLSソケットがどのようにそのリスナーに到達するかを調べます。リクエストが捕捉可能なエラーパスをたどり、捕捉されないTypeErrorが発生しなければ完了です。
索引モデルが issue の本文から書いたものです。
説明
Version
1.64.1 (also reproduced on 1.61.1; main at 4ed9744 has the same code)
Steps to reproduce
Single self-contained script, no browser needed:
npm i [email protected]openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 1 -subj "/CN=localhost"- Save the script below as
repro.cjsand runnode repro.cjs
The patch of http.Agent.prototype.addRequest only exists to put the agent deterministically into a state that happens naturally under load (see Additional context): the most recently pooled keep-alive socket has been destroyed, but its 'close' event has not fired yet, so it is still in agent.freeSockets when the next request is queued.
const fs = require('fs');
const http = require('http');
const https = require('https');
const { request } = require('playwright-core');
process.on('uncaughtException', (error) => {
console.log('uncaughtException:', error.stack);
process.exit(1);
});
const server = https.createServer(
{ key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') },
(req, res) => res.end('ok')
);
server.listen(0, async () => {
const url = `https://localhost:${server.address().port}/`;
const context = await request.newContext({ ignoreHTTPSErrors: true });
await Promise.all([context.get(url), context.get(url)]);
const originalAddRequest = http.Agent.prototype.addRequest;
http.Agent.prototype.addRequest = function (req, options) {
const pooledSockets = Object.values(this.freeSockets).find((sockets) => sockets.length > 1);
pooledSockets?.at(-1).destroy();
return originalAddRequest.call(this, req, options);
};
await context.get(url).catch((error) => console.log('request rejected (expected, catchable):', error.message));
console.log('no crash');
process.exit(0);
});
Expected behavior
The request fails through its normal error path: the context.get() promise rejects (e.g. socket hang up) and the caller can catch it.
Actual behavior
An uncaught TypeError is thrown from the request's 'socket' listener:
uncaughtException: TypeError: Cannot read properties of null (reading 'valid_from')
at captureSecurityDetails (node_modules/playwright-core/lib/coreBundle.js:28068:34)
at ClientRequest.<anonymous> (node_modules/playwright-core/lib/coreBundle.js:28092:15)
at ClientRequest.emit (node:events:519:28)
at tickOnSocket (node:_http_client:925:7)
at onSocketNT (node:_http_client:989:5)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21)
On 1.61.1 the same crash reads Cannot read properties of null (reading 'subject').
In @playwright/test this fails whichever test is running with that error (no stack in the report), while the test body keeps running until teardown. Wrapping route.fetch() / request.get() in try/catch does not help, because the throw happens in an event listener rather than in the returned promise.
Additional context
Root cause:
TLSSocket.getPeerCertificate()returnsnullonce the socket has been destroyed (it returns{}for a resumed session, which thevalid_fromcheck already handles).- Node's
Agent.addRequest()skips destroyed sockets only at the head offreeSockets[name], but with the defaultscheduling: 'lifo'it then pops the tail without checking it. A socket destroyed earlier in the same event-loop turn stays infreeSocketsuntil its'close'event fires (that is when the agent removes it), so it can be handed to a new request as a reused socket. fetch.tscallscaptureSecurityDetails(socket)synchronously whenrequest.reusedSocketis true (fetch.ts#L629-L631) and readspeerCertificate.valid_fromon thenull(fetch.ts#L599-L600).
We hit this in CI (Linux, Node 22, @playwright/test 1.61.1) without any patching. A context.route() handler called route.fetch() for an image on cdn.jsdelivr.net. 400 ms earlier, a route.fetch() for the same URL had failed with read ECONNRESET, so the CDN was resetting pooled connections at that moment. The trace shows the uncaught TypeError about 5 ms after the second route.fetch() started. That route.fetch() then rejected with socket hang up (from socketCloseListener), which our handler caught. The test had already failed on the uncaught error.
This is related to #42698 (reused keep-alive sockets failing with ECONNRESET), but different in effect: that failure is a catchable rejection, while this one escapes as an uncaught exception.
A possible fix is to bail out when there is no certificate, so the request fails through its normal error path:
const peerCertificate = socket.getPeerCertificate();
if (!peerCertificate)
return;
Environment
System:
OS: Windows 11 10.0.26200
CPU: (16) x64 Intel(R) Core(TM) Ultra 9 285H
Memory: 3.99 GB / 31.40 GB
Binaries:
Node: 22.23.2 - C:\Program Files\nodejs\node.EXE
npm: 10.9.8 - C:\Program Files\nodejs\npm.CMD
npmPackages:
playwright-core: ^1.64.1 => 1.64.1
Originally observed on Linux (CI), Node 22, @playwright/test 1.61.1.
- 主要言語
- TypeScript
- スター
- 98.5k
- フォーク
- 6.7k
- 平均マージ
- 15時間 42分
- マージ済み PR(30日)
- 313
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
microsoft/playwright のほかの issue
-
needs-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
microsoft/playwright#43762 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
microsoft/playwright#43759 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
needs-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
microsoft/playwright#43741 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
[Feature]: Set `this.name` in `TargetClosedError` constructor再び着手できるかも @LeonxLJX が 56 日前に担当しましたが、オープン中のプルリクエストはありません。 オープンP3-collecting-feedback
難易度 1/5 1時間未満 初心者へのやさしさ 86/100
microsoft/playwright#42522 · コメント 7 件 ·
メンテナーはふだん 1 日以内に返信
-
[Feature]: Support sharing self-contained HTML reports with embedded screenshots and attachmentsオープン
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
microsoft/playwright#43771 ·
メンテナーはふだん 1 日以内に返信
microsoft/playwright の issue をすべて見る
似ている issue
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 86/100
awslabs/visual-asset-management-system#937 ·
メンテナーはふだん 1 日以内に返信
-
t0ggles
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
nolimits4web/swiper#8255 · コメント 1 件 ·
メンテナーはふだん 15 日以内に返信
-
render: --variables-file (and --variables <path>) fails with "io.readFile is not a function" since 0.8.212対応中かも @miguel-heygen が今日担当しました。 オープンbug triage/needs-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
heygen-com/hyperframes#6830 ·
メンテナーはふだん 1 日以内に返信
-
bug launch local use
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
メンテナーはふだん 1 日以内に返信
-
account: profile bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 5 日以内に返信