RightNow-AI/openfang · warning · BrowserResponse

Timed out waiting for selector: {selector} ({max_ms}ms)

Error message

Timed out waiting for selector: {selector} ({max_ms}ms)

What it means

cmd_wait polls the page via CDP (document.querySelector) every PAGE_LOAD_POLL_INTERVAL_MS until the selector matches or the timeout (capped at 30s) expires. When the element never appears within max_ms, the loop ends and this error is returned. It indicates the element was not present in the DOM in time, not a transport failure — each poll that errors is silently skipped.

Source

Thrown at crates/openfang-runtime/src/browser.rs:593

    async fn cmd_wait(&self, selector: &str, timeout_ms: u64) -> BrowserResponse {
        let sel_json = serde_json::to_string(selector).unwrap_or_default();
        let max_ms = timeout_ms.min(30_000);
        let polls = (max_ms / PAGE_LOAD_POLL_INTERVAL_MS).max(1);

        for _ in 0..polls {
            let js = format!("document.querySelector({sel_json}) ? 'found' : null");
            if let Ok(val) = self.cdp.run_js(&js).await {
                if val.as_str() == Some("found") {
                    return BrowserResponse::ok(
                        serde_json::json!({"found": true, "selector": selector}),
                    );
                }
            }
            tokio::time::sleep(Duration::from_millis(PAGE_LOAD_POLL_INTERVAL_MS)).await;
        }

        BrowserResponse::err(format!(
            "Timed out waiting for selector: {selector} ({max_ms}ms)"
        ))
    }

    async fn cmd_run_js(&self, expression: &str) -> BrowserResponse {
        match self.cdp.run_js(expression).await {
            Ok(val) => BrowserResponse::ok(serde_json::json!({"result": val})),
            Err(e) => BrowserResponse::err(format!("JS execution failed: {e}")),
        }
    }

    async fn cmd_back(&self) -> BrowserResponse {
        match self.cdp.run_js("history.back(); 'ok'").await {
            Ok(_) => {
                tokio::time::sleep(Duration::from_millis(500)).await;
                self.wait_for_load().await;
                match self.page_info().await {
                    Ok(info) => BrowserResponse::ok(info),

View on GitHub (pinned to acf2587e46)

Solutions

  1. Verify the selector matches in the page (run document.querySelector(selector) !== null via cmd_run_js) and fix typos/syntax.
  2. Increase the timeout_ms argument (it is capped at 30000ms; request the cap explicitly for slow pages).
  3. If the element lives in an iframe or shadow DOM, run JS that reaches into the frame/shadow root instead of a plain selector.
  4. Wait for a stable ancestor or a network-idle condition first, then wait for the selector.
  5. Check that the browser session is still on the expected URL (page_info) — a redirect may have changed the DOM.

Example fix

// before: fixed short timeout, fragile selector
browser.execute("wait", json!({"selector": ".btn.primary", "timeout_ms": 2000}))
// after: cap timeout and verify selector existence first
let check = browser.cmd_run_js("document.querySelector('.btn.primary') !== null");
if !matches!(check.found, Some(true)) {
    browser.cmd_wait(".btn.primary", 30_000);
}
Defensive patterns

Strategy: retry

Validate before calling

let exists = browser.cmd_run_js(format!("document.querySelector({}) !== null", serde_json::to_string(selector).unwrap_or_default()));
if matches!(exists.result.as_str(), Some("false")) && !selector_is_shadow_dom_safe(selector) { /* pick a different anchor or raise timeout */ }

Type guard

fn is_timeout_response(resp: &BrowserResponse) -> bool {
    resp.error.as_deref().map(|e| e.starts_with("Timed out waiting for selector")).unwrap_or(false)
}

Try / catch

match browser.cmd_wait(selector, 30_000) {
    r if r.is_ok() => r,
    r if is_timeout_response(&r) => fallback_query_via_run_js(selector), // iframe/shadow-dom aware
    r => r,
}

Prevention

When it happens

Trigger: Calling the wait command with a CSS selector that never matches: wrong selector syntax, element rendered only after user interaction, content loaded via slow XHR/fetch beyond the (30s-capped) timeout, element inside an iframe (querySelector does not cross frames), or the CDP connection failing on every poll so 'found' is never seen.

Common situations: Testing SPAs where the target node mounts late or is removed on re-render; typos in class names; selectors for elements behind lazy-loading or pagination; waiting on shadow-DOM content that document.querySelector cannot see; the browser tab having navigated away so the selector legitimately no longer exists.

Understand the failure class

Related errors


AI-assisted analysis of RightNow-AI/openfang@acf2587e46 (2026-09-02). Data as JSON: /api/errors/84e581454be484ca. Report an issue: GitHub.