云浏览器限流就罢工?browser_exec 现在会自己找退路

你配好了云端浏览器,让 Hermes 每天早上截一张后台报表发到群里。头几天都很顺,直到某天早上你只看到一条错误:returned no CDP endpoint, so Browser Use mode cannot drive it。网页没问题,你的脚本没问题,是云端的浏览器服务那一刻在限流——但你的浏览器任务就这么整条挂了。
更别扭的是:Hermes 内置的那几个 browser_* 工具在同样的情况下活得好好的。只有 browser_exec 会死。
两个把浏览器卡死的场景
这次修的是两个症状,都发生在同一件事上:配置了云浏览器(browser.cloud_provider,可选 browserbase、browser-use、camofox、nous,或 local)之后,本机应该还有一条本地退路,但 browser_exec 走不到。
一、云端创建会话失败,整个调用直接失败
云 provider 在创建会话这一步失败时,browser_exec 会整个失败,错误信息是:
Cloud browser provider BrowserUseBrowserProvider returned no CDP endpoint,
so Browser Use mode cannot drive it. Switch to the built-in browser tools
for this provider.
真实见到的失败是限流:网关回了 429 RATE_LIMIT_EXCEEDED,带 retryAfter 17、limit 200。
根因在于 browser_exec 和内置工具走的不是同一条路。内置浏览器工具有一层回退:本地会话创建失败时切到打包的本地 Chromium,并在会话上标记 fallback_from_cloud。browser_exec 却只从那个会话里读 cdp_url——而本地会话在 Chromium 真正启动之前根本没有 cdp_url。于是它报了上面那条错,把锅甩给了 provider。
还有个放大器:失败的会话会被缓存。同一个 session key 的后续调用会一直撞在同一份坏会话上,直到不活跃回收器把它清掉。也就是说,一次 429 能让你后面几分钟的浏览器调用全部失败。
修法很克制:在 tools/browser_use_cli.py::_resolve_backend_cdp 里,当 provider 会话带着 fallback_from_cloud 回来时,按无 provider 路径同样的方式解析打包 Chromium 的 CDP 端点,并把这次 provider 失败只记一次日志。一个既没有 CDP URL、也没有回退的 provider,报错保持不变——不会假装一切正常。
二、云浏览器够不到你的 localhost
第二个症状更常见:本机跑着一个开发服务器,你让 Hermes 去看 http://localhost:3000,得到的是:
Blocked: URL targets a private or internal address
browser_exec 在执行前会先扫一遍代码里的 URL 做安全检查,但它调用安全检查时没有带上“混合路由”的判断。一旦配了 cloud_provider,这个判断就认为后端不是本地的,于是每一个私有地址都被拦掉。
有意思的是 browser_navigate 没有这个问题——它一直会把私有 URL 路由到本地 sidecar。同一个浏览器子系统,两条路径两套行为,这次把它们对齐了。
现在是怎么走的
| 你访问的地址 | 谁来打开 |
|---|---|
https://example.com |
配置的云浏览器 |
http://localhost:3000 |
本地 Chromium sidecar,::local 缓存键 |
http://192.168.1.9(局域网) |
本地 Chromium sidecar |
169.254.169.254(云元数据) |
无条件拦截,不给任何后端 |
| 一次调用里公私混用 | 整条调用全部跑在本地 |
实现上有几处细节值得知道,因为它们直接关系到安全:
- 路由判定
_hybrid_routes_locally(url)现在由browser_navigate和browser_exec共用,不会再有“一边能一边不能”; - 代码里出现的每个 URL 只评估一次,同一个判定同时决定安全检查结果和实际路由。这样一来,一个域名如果先解析成内网地址、再重绑定成公网地址,也没法用“先放松检查、再送去云端”这个顺序绕过去;
- 混合内容(一次调用里既有公网又有内网 URL)整条跑本地,私有的那一部分不会漏给云端;
- sidecar 使用
::local缓存键,并且跑在自己的 harness daemon(BU_NAME=_local_<name>)下。daemon 只在启动时读一次 CDP 环境变量,如果和云路由共用同一个 daemon,私有 URL 就会被转给云浏览器——所以这里必须分开; - 一次“没有 URL 字面量”的后续调用,会沿用上一次调用走过的路由。
安全边界没有被放松
回退很容易做成安全漏洞,所以这里的不变量都保留了:
- 云元数据地址仍然无条件拦截:
169.254.169.254依旧得到“URL targets a cloud metadata endpoint”,与 provider 是谁无关; browser.auto_local_for_private_urls默认是开着的,把它设成false之后,私有 URL 仍然会被拦(除非再显式打开browser.allow_private_urls,那会让云 provider 去尝试——通常不成功,因为 Browserbase 这类服务根本到不了你的局域网);- CDP override 不算本地:你自己指定了
browser.cdp_url或用了/browser connect,混合路由判定会返回“非本地”,行为保持原样; - 公网跳内网的重定向被拒绝,而不是放行;
- 导航结束后对落地 URL 的复检没有改动。
本地退路需要本地 sidecar,也就是和纯本地模式同一个 agent-browser CLI。装它最简单的办法是走 hermes setup tools → Browser Automation,会一起装好。
状态与升级
这两个修复(PR #136447 与 #136442)2026-10-11 合并进 main。当前最新标签 v0.21.6(10 月 8 日发布)里还没有它们,下一个 release 会带上。想现在就用,可以从 main 更新安装。
升级后想确认回退真的生效,可以看日志里那行 provider 失败记录——它只会出现一次,然后浏览器调用继续跑在本地 Chromium 上。
小结
云端浏览器又快又省事,但它是一个外部服务,会限流、会宕机。真正好用的配置不是“永远走云端”,而是云端优先、本地兜底,并且敏感地址永远不出本机。这次改动补的就是这条兜底路径:provider 挂了你的任务继续跑,开发服务器和局域网页面照常能开,而内网地址和云元数据地址一个都没被送出去。
相关阅读:内置浏览器工具成为默认讲了云 provider 的选择逻辑,浏览器快照的预算控制解决的是另一种“浏览器很慢”的问题,免费额度下的浏览器验证挑战则是把云浏览器用在省钱方案里的取舍。