Skip to main content

dravr_browser/
error.rs

1// ABOUTME: Error type for headless-browser automation: launch, navigation, interaction, auth, config
2// ABOUTME: Consumers convert BrowserError into their own domain error (RunnerError, ScraperError, etc.)
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7/// Result alias for browser operations.
8pub type BrowserResult<T> = Result<T, BrowserError>;
9
10/// Errors arising from headless-browser automation.
11#[derive(Debug, thiserror::Error)]
12pub enum BrowserError {
13    /// Browser launch, connection, or low-level CDP failure.
14    #[error("browser error: {reason}")]
15    Browser {
16        /// Detailed failure reason.
17        reason: String,
18    },
19
20    /// Page navigation failure.
21    #[error("navigation error: {reason}")]
22    Navigation {
23        /// Detailed failure reason.
24        reason: String,
25    },
26
27    /// DOM interaction failure (element not found, click/fill failed).
28    #[error("interaction error: {reason}")]
29    Interaction {
30        /// Detailed failure reason.
31        reason: String,
32    },
33
34    /// Authentication / session failure (no cookies captured, expired session).
35    #[error("auth error: {reason}")]
36    Auth {
37        /// Detailed failure reason.
38        reason: String,
39    },
40
41    /// Configuration error (invalid launch config, missing values).
42    #[error("config error: {reason}")]
43    Config {
44        /// Detailed failure reason.
45        reason: String,
46    },
47
48    /// Operation exceeded its deadline.
49    #[error("timeout: {reason}")]
50    Timeout {
51        /// Detailed failure reason.
52        reason: String,
53    },
54}
55
56impl BrowserError {
57    /// Construct a [`BrowserError::Browser`].
58    pub fn browser(reason: impl Into<String>) -> Self {
59        Self::Browser {
60            reason: reason.into(),
61        }
62    }
63
64    /// Construct a [`BrowserError::Navigation`].
65    pub fn navigation(reason: impl Into<String>) -> Self {
66        Self::Navigation {
67            reason: reason.into(),
68        }
69    }
70
71    /// Construct a [`BrowserError::Interaction`].
72    pub fn interaction(reason: impl Into<String>) -> Self {
73        Self::Interaction {
74            reason: reason.into(),
75        }
76    }
77
78    /// Construct a [`BrowserError::Auth`].
79    pub fn auth(reason: impl Into<String>) -> Self {
80        Self::Auth {
81            reason: reason.into(),
82        }
83    }
84
85    /// Construct a [`BrowserError::Config`].
86    pub fn config(reason: impl Into<String>) -> Self {
87        Self::Config {
88            reason: reason.into(),
89        }
90    }
91
92    /// Construct a [`BrowserError::Timeout`].
93    pub fn timeout(reason: impl Into<String>) -> Self {
94        Self::Timeout {
95            reason: reason.into(),
96        }
97    }
98
99    /// Whether this error is transient and may succeed on retry.
100    #[must_use]
101    pub const fn is_transient(&self) -> bool {
102        matches!(
103            self,
104            Self::Browser { .. } | Self::Navigation { .. } | Self::Timeout { .. }
105        )
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn transient_classification() {
115        assert!(BrowserError::browser("x").is_transient());
116        assert!(BrowserError::timeout("x").is_transient());
117        assert!(!BrowserError::auth("x").is_transient());
118        assert!(!BrowserError::config("x").is_transient());
119    }
120
121    #[test]
122    fn display_includes_reason() {
123        assert_eq!(
124            BrowserError::interaction("no element").to_string(),
125            "interaction error: no element"
126        );
127    }
128}