Skip to main content

servo_fetch/
error.rs

1//! Error types.
2
3use std::time::Duration;
4
5/// A specialized `Result` type for servo-fetch.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors from servo-fetch operations.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum Error {
12    /// The URL is malformed or uses a disallowed scheme.
13    #[error("{reason}")]
14    InvalidUrl {
15        /// The URL that failed validation.
16        url: String,
17        /// Why the URL is invalid.
18        reason: String,
19    },
20
21    /// The page did not finish loading within the configured timeout.
22    #[error("page load timed out after {}s", timeout.as_secs())]
23    Timeout {
24        /// The URL that timed out.
25        url: String,
26        /// The timeout that was exceeded.
27        timeout: Duration,
28    },
29
30    /// The URL resolves to a private or reserved address (SSRF protection).
31    #[error("address not allowed: {0}")]
32    AddressNotAllowed(String),
33
34    /// The Servo engine is unavailable or crashed.
35    #[error("engine error: {0}")]
36    Engine(String),
37
38    /// JavaScript evaluation failed.
39    #[error("JavaScript evaluation failed: {0}")]
40    JavaScript(String),
41
42    /// Screenshot capture failed.
43    #[error("screenshot capture failed: {0}")]
44    Screenshot(String),
45
46    /// Content extraction failed.
47    #[error(transparent)]
48    Extract(#[from] crate::extract::ExtractError),
49
50    /// Schema-based structured extraction failed.
51    #[error(transparent)]
52    Schema(#[from] crate::schema::SchemaError),
53
54    /// An I/O error occurred.
55    #[error(transparent)]
56    Io(#[from] std::io::Error),
57
58    /// A glob pattern is invalid.
59    #[error("invalid glob pattern: {0}")]
60    InvalidGlob(#[from] globset::Error),
61}
62
63impl Error {
64    /// Returns `true` if this is a timeout error.
65    #[must_use]
66    pub fn is_timeout(&self) -> bool {
67        matches!(self, Self::Timeout { .. })
68    }
69
70    /// Returns `true` if this is a network-related error.
71    #[must_use]
72    pub fn is_network(&self) -> bool {
73        matches!(self, Self::Timeout { .. } | Self::AddressNotAllowed(_))
74    }
75
76    /// Returns the URL associated with this error, if any.
77    #[must_use]
78    pub fn url(&self) -> Option<&str> {
79        match self {
80            Self::InvalidUrl { url, .. } | Self::Timeout { url, .. } | Self::AddressNotAllowed(url) => Some(url),
81            _ => None,
82        }
83    }
84}
85
86#[allow(clippy::used_underscore_items)]
87const _: () = {
88    fn _assert<T: Send + Sync>() {}
89    fn _check() {
90        _assert::<Error>();
91    }
92};
93
94/// Why a URL was rejected by [`validate_url`].
95#[derive(Debug)]
96pub(crate) enum UrlError {
97    /// Malformed URL or disallowed scheme.
98    Invalid(String),
99    /// Host resolves to a private or reserved address.
100    PrivateAddress(String),
101}
102
103impl std::fmt::Display for UrlError {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        match self {
106            Self::Invalid(reason) => f.write_str(reason),
107            Self::PrivateAddress(host) => {
108                write!(f, "access to private/local addresses is not allowed: {host}")
109            }
110        }
111    }
112}
113
114pub(crate) fn map_url_error(url: &str, e: UrlError) -> Error {
115    match e {
116        UrlError::PrivateAddress(host) => Error::AddressNotAllowed(host),
117        UrlError::Invalid(reason) => Error::InvalidUrl {
118            url: url.into(),
119            reason,
120        },
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn timeout_is_timeout() {
130        let err = Error::Timeout {
131            url: "https://example.com".into(),
132            timeout: Duration::from_secs(30),
133        };
134        assert!(err.is_timeout());
135        assert!(err.is_network());
136        assert_eq!(err.url(), Some("https://example.com"));
137    }
138
139    #[test]
140    fn address_not_allowed_is_network() {
141        let err = Error::AddressNotAllowed("127.0.0.1".into());
142        assert!(!err.is_timeout());
143        assert!(err.is_network());
144        assert_eq!(err.url(), Some("127.0.0.1"));
145    }
146
147    #[test]
148    fn invalid_url_has_url() {
149        let err = Error::InvalidUrl {
150            url: "bad://url".into(),
151            reason: "scheme not allowed".into(),
152        };
153        assert!(!err.is_timeout());
154        assert!(!err.is_network());
155        assert_eq!(err.url(), Some("bad://url"));
156    }
157
158    #[test]
159    fn engine_error_has_no_url() {
160        let err = Error::Engine("crashed".into());
161        assert!(!err.is_timeout());
162        assert!(err.url().is_none());
163    }
164}