[fix]: bound navigations by their timeout before the navigate command returns

FO
5d ago
+55-14

why

goto, reload, goBack and goForward create a LifecycleWatcher with the call's timeout, but the watcher only started counting in wait(), which runs after the CDP command that starts the navigation returns. Chrome answers Page.navigate only once the navigation commits or fails, so the time spent waiting for the server sat outside the timeout:

  • A server that accepts the connection but never responds leaves goto pending with no bound inside the extension. The caller gets the client's RPC response timed out after 25000ms: page.goto (the 15s default plus the 10s RPC grace) instead of a navigation timeout.
  • A server that takes T to respond gets T + timeout in total, so any commit slower than the 10s RPC grace runs into the RPC deadline rather than the navigation's own timeout.

Checked against headless Chrome over CDP: Page.navigate to a local server that never responds had not answered after 30s; to one that responds after 12s, it answered at 12.0s.

what changed

  • LifecycleWatcher fixes its deadline when it is created (startTime + timeout). wait() gets what is left of it, so timeout bounds the whole navigation, as it does in Playwright and Puppeteer.
  • New awaitNavigationCommand() races the command that starts the navigation against that deadline and the watcher's abort. It rejects with the existing TimeoutError (Navigation start timed out after <timeout>ms) and shares its timer with awaitNavigationStart.
  • goto, reload, goBack and goForward await their CDP command through it. Only Page.navigate is known to wait for commit. The other three are wrapped so timeout means the same thing for all four.

Behavior change: a navigation with a slow commit now gets timeout in total rather than timeout after the commit. As before, a timed-out navigation keeps running in the browser.

test plan

  • New case in packages/extension/tests/page-navigation-response.test.ts: Page.navigate never answers, and goto({ timeout: 50 }) rejects with Navigation start timed out after 50ms. On main the same case hangs until vitest's test timeout.
  • Extension unit tests: 1,189 passed. The 3 extension-build tests need pnpm run build first.
  • turbo run fmt:check lint typecheck --filter=@browserbasehq/stagehand-extension passes.
  • Python checks weren't run locally.

🤖 Generated with Claude Code


Summary by cubic

goto, reload, goBack, and goForward now bound the entire navigation by the timeout option, including the time the browser takes to acknowledge the navigation start. Previously the timeout started only after the CDP command returned, so a server that accepted the connection but never responded left goto hanging until the client's RPC deadline fired.

  • The LifecycleWatcher fixes its deadline at construction and races the navigation-starting command against it.
  • A navigation with a slow commit now takes timeout in total instead of timeout after the commit.
  • A timed-out navigation still keeps running in the browser, as before.