[Bug]: Uncaught TypeError in captureSecurityDetails when APIRequestContext reuses a destroyed keep-alive socket
Maintainer thường phản hồi trong vòng 1 ngày
@microsoft-playwright-automation đang làm issue này rồi.
Từ ngày 28/10/2026.
- #43729 của @microsoft-playwright-automation — đang mở
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 76/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- node.js, typescript
- Lĩnh vực
- backend, networking
Hướng nghiên cứu
Bắt đầu tại packages/playwright-core/src/server/fetch.ts, ở lời gọi captureSecurityDetails được báo cáo và thao tác đọc chứng chỉ. Tái hiện bằng script Node độc lập trong issue, sau đó kiểm tra cách một TLS socket được tái sử dụng nhưng đã bị hủy đi tới listener đó. Hoàn thành khi yêu cầu đi theo nhánh lỗi có thể bắt được mà không phát sinh TypeError không được bắt.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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.
- Ngôn ngữ chính
- TypeScript
- Star
- 98.5k
- Fork
- 6.7k
- Merge trung bình
- 16 giờ 10 phút
- Pull request đã merge (30 ngày)
- 305
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của microsoft/playwright
-
[Feature]: Set `this.name` in `TargetClosedError` constructorCó thể làm lại được @LeonxLJX đã nhận 55 ngày trước và không có pull request nào đang mở. Đang mởP3-collecting-feedback
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 86/100
microsoft/playwright#42522 · 7 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
test.fail() in after hooks relabels an already-executed test and can make a failing run exit 0Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 72/100
microsoft/playwright#43738 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 42/100
microsoft/playwright#43715 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 68/100
microsoft/playwright#43711 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 25/100
microsoft/playwright#43709 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của microsoft/playwright
Issue tương tự
-
area:plugins area:security area:skills bug has repro
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
anthropics/claude-code#105911 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
openspec-apply-change skill: when blocked, may select a ready artifact unrelated to missingArtifactsĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Fission-AI/OpenSpec#2165 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
🐞 bug 🧐 unconfirmed
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
Tencent/tdesign-vue-next#7032 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
gnosis/gnosis_vpn#661 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
content-freshness: staleOwnedContent doc comments still say 'complete receipt' (follow-up to #7912)Đang mởtriage:bug-verified
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 92/100
Maintainer thường phản hồi trong vòng 1 ngày