[fix]: bound navigations by their timeout before the navigate command returns
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
gotopending with no bound inside the extension. The caller gets the client'sRPC 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 +
timeoutin 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
LifecycleWatcherfixes its deadline when it is created (startTime + timeout).wait()gets what is left of it, sotimeoutbounds 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 existingTimeoutError(Navigation start timed out after <timeout>ms) and shares its timer withawaitNavigationStart. goto,reload,goBackandgoForwardawait their CDP command through it. OnlyPage.navigateis known to wait for commit. The other three are wrapped sotimeoutmeans 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.navigatenever answers, andgoto({ timeout: 50 })rejects withNavigation start timed out after 50ms. Onmainthe same case hangs until vitest's test timeout. - Extension unit tests: 1,189 passed. The 3
extension-buildtests needpnpm run buildfirst. turbo run fmt:check lint typecheck --filter=@browserbasehq/stagehand-extensionpasses.- 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
LifecycleWatcherfixes its deadline at construction and races the navigation-starting command against it. - A navigation with a slow commit now takes
timeoutin total instead oftimeoutafter the commit. - A timed-out navigation still keeps running in the browser, as before.