use thiserror::Error;
#[derive(Debug, Error)]
pub enum SessionError {
#[error("tab hung: op exceeded {timeout_ms}ms without reply ({hint}) [target={target_id:?} url={url:?}]")]
TabHung {
target_id: Option<String>,
url: Option<String>,
timeout_ms: u64,
hint: &'static str,
},
#[error("tab crashed: {reason} [target={target_id:?}]")]
TabCrashed { target_id: String, reason: String },
#[error("no tab `{name}` registered for browser `{browser}` — run `browser-control tab open {browser}/{name}` first")]
TabNotFound { browser: String, name: String },
#[error("{kind:?} target gone: {details}")]
TargetGone { kind: TargetKind, details: String },
#[error("tool `{tool}` requires {required_engine} engine; current browser uses {current_engine} ({hint})")]
EngineUnsupported {
tool: String,
required_engine: String,
current_engine: String,
hint: &'static str,
},
#[error(
"Playwright sidecar connection failed while running `{tool}` ({method}) against target {target_id:?} url {url:?}: {details}. browser-control reached the tab with native CDP, so this is a Playwright sidecar/CDP attachment failure, not evidence that the page is hung ({hint})"
)]
SidecarConnectionFailed {
tool: String,
method: String,
target_id: String,
url: Option<String>,
details: String,
hint: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetKind {
Cdp,
Bidi,
}
pub const CDP_TARGET_GONE_NEEDLES: &[&str] = &[
"no target with given id",
"session is gone",
"no session with given id",
"target closed",
];
pub const BIDI_TARGET_GONE_NEEDLES: &[&str] = &[
"no such frame",
"no such node",
"no such context",
"invalid session id",
];
pub fn is_cdp_target_gone(message: &str) -> bool {
let m = message.to_ascii_lowercase();
CDP_TARGET_GONE_NEEDLES.iter().any(|n| m.contains(n))
}
pub fn is_bidi_target_gone(code: &str, message: &str) -> bool {
let c = code.to_ascii_lowercase();
if BIDI_TARGET_GONE_NEEDLES.iter().any(|n| c.contains(n)) {
return true;
}
let m = message.to_ascii_lowercase();
BIDI_TARGET_GONE_NEEDLES.iter().any(|n| m.contains(n))
}
pub fn is_recoverable_tab_failure(err: &anyhow::Error) -> bool {
if let Some(se) = err.downcast_ref::<SessionError>() {
return matches!(
se,
SessionError::TabHung { .. }
| SessionError::TabCrashed { .. }
| SessionError::TargetGone { .. }
);
}
let msg = format!("{err:#}").to_ascii_lowercase();
CDP_TARGET_GONE_NEEDLES.iter().any(|n| msg.contains(n))
|| BIDI_TARGET_GONE_NEEDLES.iter().any(|n| msg.contains(n))
}