返回博客

国内 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>/

坑:sentinel 只检 chrome.exe,写假阳性 marker

桌面 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
}
正解:npmmirror chrome-for-testing 直接拉,两个 zip 都要

chrome-for-testingchrome-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 必须同时下,少一个等于没装。