页面正在导航与主 Frame 缺失:PAGE_NAVIGATING
只读命令突然报 Cannot invoke "com.microsoft.playwright.Frame.childFrames()" because "frame" is null,或者在构建快照时抛 Index 0 out of bounds for length 0 —— 这两种异常看起来毫不相干,实际是同一件事:这一刻页面的主 frame 是 null。本章说明它从哪来、服务端怎么分类,以及 frame 枚举失效时怎么继续把任务做完。
1. 症状与成因
| 症状 | 出现位置 |
|---|---|
Cannot invoke "...Frame.childFrames()" because "frame" is null | get_browser_state、list_frames、get_modals,以及带 frame 参数的选择器类命令 |
构建页面结构失败:Index 0 out of bounds for length 0 | get_browser_state 构建快照时 |
常见成因:
- 页面正在导航或整页重建(用户手动刷新、SPA 整页重建、micro-app 重新挂载);
- 页面停在一个重定向循环上:地址栏在登录域与目标站之间来回跳,主 frame 一直没有稳定下来(第 4 节是真实案例);
- 某些新版控制台的「lite」页面,页面对象一直不给出主 frame。
它不该被当成「动作可能已生效」。以前这类异常会冒到兜底分支、被归成 ACTION_UNCERTAIN(「无法判断是否已生效,别重试」)—— 而这明明是等一会儿就好的瞬时状态,方向正好相反。
2. 服务端怎么处理
链路上一共四处改动,合起来把这条路走通:
DomService.frameTree:主 frame 为 null 时不再让 NPE 冒出去,而是抛一个带page_navigating哨兵的异常。返回空列表会被误读成「空白页」,哨兵才能表明「这一刻只是拿不到 DOM」。ActionError.isPageNavigating:认得这个哨兵(也认得驱动给的because "frame" is null、execution context was destroyed等措辞),归成PAGE_NAVIGATING,并按可重试处理。PlaywrightService.getBrowserStateAttempt:快照构建的 catch 从PlaywrightException放宽到RuntimeException(NPE 与越界都包进来),是导航类失败就打上PAGE_NAVIGATING前缀。ActionService.dispatchWithSpuriousRetry:重发不再只认伪故障,页面正在导航时也对可重发名单里的命令(只读 / 幂等导航 / 覆盖式落盘 / 等待)自动重试,成功后回执里给data.pageNavigatingRetry,与伪故障的data.spuriousRetry分开记。
只读命令仍失败时,兜底分支给出的是:
{
"errorCode": "PAGE_NAVIGATING",
"retryable": true,
"pageNavigating": true,
"retryAfterMs": 1000,
"note": "页面正在导航/重建,这一刻拿不到 DOM;这是只读命令,重发无害,稍后再发即可。"
}
动作类命令不会因此被自动重发:回执 retryable:false、提示先读页面状态。这一点与 防重复提交 的原则一致 —— 页面在导航不代表动作没生效。
3. 调用方怎么用
- 只读命令:看到
PAGE_NAVIGATING且retryable:true,按retryAfterMs(约 1 秒)重发即可;多数情况下服务端已经替你重试过(看pageNavigatingRetry.attempts)。 - 动作类命令:不要自动补点。先用
get_page_snapshot/get_browser_state确认上一次到底生效没有,再决定。 - 判断页面稳不稳定:连读两次
get_page_snapshot看 URL 是否还在变;地址一直在跳就属于第 4 节那种情况,别再等它「自己好」。
4. 实战:控制台一直重定向,怎么都到不了
实测(2026-10)要打开阿里云账单控制台,按直觉访问 billing.console.aliyun.com:地址栏立刻变成 account.aliyun.com/login/login.htm?oauth_callback=…,然后在 account.aliyun.com 与 account.alibabacloud.com 之间来回弹,永远进不去;这一页上 get_browser_state / list_frames 一直报主 frame 为 null。真相是账单控制台的真实主机名是 billing-cost.console.aliyun.com(只差一个 -cost),旧主机名只是把你弹回登录页。
排查顺序:
- 看到「一直在登录页打转」先怀疑主机名,不要在这一页上找元素、换选择器、反复填登录表单;
- 回一个已经登录的控制台,用
execute_js把导航链接的href全读出来,从真实链接里照抄主机名与路径:[...document.querySelectorAll('a')].map(a => ({ t: a.innerText.trim(), h: a.href })); - 用
get_page_snapshot确认 URL 稳定下来,再继续后续步骤。
5. 实战:frame 枚举失效时,怎么操作跨域 iframe
主 frame 为 null 时,受影响的不只是快照:frameOf 内部同样走 frameTree,所以带 frame 参数的选择器命令也会失败,跨域 iframe(例如短信验证码弹层)就「够不着」了。此时可以按坐标操作:
execute_js量出 iframe 的位置:document.querySelector('iframe[src*="…"]').getBoundingClientRect();screenshot传selector=iframe[src*="…"],只截那一个 iframe(默认落盘,回data.path);- 用
ocr_image读出上面的文字;验证码这类「文本表达不了」的内容可以直接看图确认布局; - 用
mouse_click按坐标点击;鼠标点不动的按钮改用键盘:先点一下输入框让 iframe 拿到焦点,再send_keys的Tab+Enter; - 每一步之后回读页面(
execute_js取innerText)确认状态,而不是假设点中了。
这条路的代价是「看不见结构化元素」,所以顺序不能省:先量位置 → 截图确认 → 再点;点完必须回读。
6. 相关代码与测试
| 位置 | 作用 |
|---|---|
DomService.frameTree | 主 frame 为 null → 抛带 page_navigating 哨兵的异常 |
ActionError.isPageNavigating / code | 归类成 PAGE_NAVIGATING,标记可重试 |
PlaywrightService.getBrowserStateAttempt | 快照构建兜住 RuntimeException,打上分类前缀 |
ActionService.dispatchWithSpuriousRetry / retryReason | 只读命令在导航/伪故障两类起因下自动重试,分别记 pageNavigatingRetry / spuriousRetry |
钉住这些行为的测试:ActionErrorTest.pageNavigatingIsItsOwnRetryableCode、 SpuriousDispatchRetryTest.readOnlyCommandRetriesWhilePageIsNavigating、 SpuriousDispatchRetryTest.actionCommandIsNotRetriedWhilePageIsNavigating。
相关章节:统一命令接口与错误码、执行链与异常处理、 DOM 与跨 Frame 索引、页面状态与元素读取、等待与重试边界。
