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
- Verify the selector matches in the page (run document.querySelector(selector) !== null via cmd_run_js) and fix typos/syntax.
- Increase the timeout_ms argument (it is capped at 30000ms; request the cap explicitly for slow pages).
- If the element lives in an iframe or shadow DOM, run JS that reaches into the frame/shadow root instead of a plain selector.
- Wait for a stable ancestor or a network-idle condition first, then wait for the selector.
- 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
- Validate selectors against the live DOM with cmd_run_js before waiting on them.
- Use the maximum timeout (30000ms) for SPA or slow-network pages.
- Prefer waiting on elements that persist (layout containers) over transient ones.
- Remember document.querySelector cannot see into iframes or shadow roots — use JS for those.
- After navigation, wait for load state before waiting on selectors of the new page.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- JS execution failed: {e}
- Back succeeded but page info failed: {e}
- Back failed: {e}
- Navigate failed: {e}
- Navigate succeeded but page info failed: {e}
AI-assisted analysis of RightNow-AI/openfang@acf2587e46 (2026-09-02).
Data as JSON: /api/errors/84e581454be484ca.
Report an issue: GitHub.