国内 Playwright 装浏览器卡死:ECONNRESET + 只装一半的双重坑
在一套桌面 RPA 应用的发布流水线里,有一步需要在 Windows 构建机上安装 Playwright 的 Chromium。命令很简单:npx playwright install chromium。本地 Mac 开发时一次过,到了国内 Windows 构建机上,进度条永远停在 0%。
这是两个连环坑:第一个让你装不上,第二个让你以为装上了但实际跑不起来。
现象
Downloading Chrome for Testing 148.0.7778.96 (playwright chromium v1223)
from https://cdn.playwright.dev/builds/cft/148.0.7778.96/win64/chrome-win64.zip
Error: read ECONNRESET
Downloading Chrome for Testing 148.0.7778.96 ...
| | 0% of 181.9 MiB
内置重试三次,每次都 ECONNRESET,或者进度卡在 0% 永不动。cdn.playwright.dev 走 Azure 边缘节点,国内访问本就不稳定,同一台机不同时段表现还不一样——偶尔通了,会误判为"网络没问题"。
根因
坑 1:PLAYWRIGHT_DOWNLOAD_HOST 镜像路径不匹配
第一反应是换镜像。文档里有 PLAYWRIGHT_DOWNLOAD_HOST 环境变量,设成 npmmirror 看起来对:
PLAYWRIGHT_DOWNLOAD_HOST=https://registry.npmmirror.com/-/binary/playwright但 playwright 内部拼接 URL 是 ${HOST}/builds/cft/<CFT_VER>/<PLATFORM>/chrome-<PLATFORM>.zip,而 npmmirror 的 playwright 镜像里 chromium 子目录只同步到了 revision 1200,1223 不存在,换了 host 照样 404。
坑 2:只装 chrome.zip,headless 必报 Executable doesn't exist
即便用其他方式把 Chromium zip 下下来、解压、写好 marker 文件,第一次 chromium.launch({ headless: true }) 就炸:
browserType.launch: Executable doesn't exist at
...\chromium_headless_shell-1223\chrome-headless-shell-win64\chrome-headless-shell.exe
原因是 playwright 1.46+ 把 Chrome for Testing 拆成了两个 artifact:完整 chrome-<platform>.zip 和精简的 chrome-headless-shell-<platform>.zip。playwright 1.59+ 默认 headless: true 走的是第二个目录 chromium_headless_shell-<ver>/,而不是第一个 chromium-<ver>/。
桌面 app 首次启动常用 .installed-<ver> marker 跳过重新下载。如果 hasChromiumExecutable 只检查 chromium-<ver>/chrome.exe 是否存在就认为 ready,装了 chrome.zip 就会写下 marker——之后每次启动都命中 marker、跳过重下,但 headless launch 永远失败,重启也救不了。
正解
绕开 playwright install,直接从 npmmirror chrome-for-testing 镜像手动拉两个 zip。
npmmirror 完整镜像了 Google 官方的 Chrome for Testing CDN(和 playwright 用的同一份 binary),URL 格式:
https://registry.npmmirror.com/-/binary/chrome-for-testing/<CFT_VER>/<PLATFORM>/chrome-<PLATFORM>.zip
https://registry.npmmirror.com/-/binary/chrome-for-testing/<CFT_VER>/<PLATFORM>/chrome-headless-shell-<PLATFORM>.zip
playwright revision → CFT 版本号从 playwright install 的报错信息里直接读(Downloading Chrome for Testing 148.0.7778.96 (playwright chromium v1223)),或者查 node_modules/playwright-core/browsers.json。
两个 zip 都要装,解压到对应目录:
| 用途 | zip | 解压到 |
|------|-----|--------|
| headless: false / headed | chrome-<platform>.zip | chromium-<PW_VER>/ |
| headless: true(默认) | chrome-headless-shell-<platform>.zip | chromium_headless_shell-<PW_VER>/ |
PowerShell 完整版(Windows 构建机):
$cftVer = '148.0.7778.96'
$pwVer = '1223'
$staging = "C:\tmp\chromium-win-stage-$pwVer"
Remove-Item -Recurse -Force $staging -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force $staging | Out-Null
$base = "https://registry.npmmirror.com/-/binary/chrome-for-testing/$cftVer/win64"
# 1) 完整 chrome(headed 用)
Invoke-WebRequest -Uri "$base/chrome-win64.zip" -OutFile "$staging\chrome-win64.zip" -UseBasicParsing
New-Item -ItemType Directory -Force "$staging\chromium-$pwVer" | Out-Null
Expand-Archive "$staging\chrome-win64.zip" -DestinationPath "$staging\chromium-$pwVer" -Force
# 2) headless shell(headless: true 必须有这个)
Invoke-WebRequest -Uri "$base/chrome-headless-shell-win64.zip" -OutFile "$staging\chrome-headless-shell-win64.zip" -UseBasicParsing
New-Item -ItemType Directory -Force "$staging\chromium_headless_shell-$pwVer" | Out-Null
Expand-Archive "$staging\chrome-headless-shell-win64.zip" -DestinationPath "$staging\chromium_headless_shell-$pwVer" -Force
# tarball 含两个顶层目录,文件名加后缀避开旧缓存
tar -czf "C:\tmp\playwright-chromium-$pwVer-win-x64-with-headless-shell.tar.gz" `
-C "$staging" "chromium-$pwVer" "chromium_headless_shell-$pwVer"修复 sentinel 检查,两个目录缺一就返回 false:
function hasChromiumReady(browsersPath: string, ver: string): boolean {
return hasFullChrome(browsersPath, ver) && hasHeadlessShell(browsersPath, ver);
// 缺任意一个 → false → 触发重下,不写假阳性 marker
}chrome-for-testing 和 chrome-headless-shell 是同一 CFT 版本下的两个独立 artifact,playwright 1.59+ 的 headless 模式只走后者。sentinel 校验必须同时确认两个目录都存在,否则会写假阳性 marker。
补充:OSS 上发现 tarball 不全怎么救
如果旧 tarball 已经推到 OSS,客户端 HEAD 命中 200 就 skip 重下,改 ETag/Last-Modified 都不可靠。最简单的方式是换文件名后缀(如加 -with-headless-shell),同时改三处:release 脚本生成端、tarball 下载检查端、sidecar 编译期默认 URL。
一句话外卖
playwright 1.59+ 的 headless launch 走的是
chromium_headless_shell-<ver>/而非chromium-<ver>/——国内绕道 npmmirror chrome-for-testing 镜像时,两个 zip 必须同时下,少一个等于没装。