1use std::time::Duration;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum Error {
12 #[error("{reason}")]
14 InvalidUrl {
15 url: String,
17 reason: String,
19 },
20
21 #[error("page load timed out after {}s", timeout.as_secs())]
23 Timeout {
24 url: String,
26 timeout: Duration,
28 },
29
30 #[error("address not allowed: {0}")]
32 AddressNotAllowed(String),
33
34 #[error("engine error: {0}")]
36 Engine(String),
37
38 #[error("JavaScript evaluation failed: {0}")]
40 JavaScript(String),
41
42 #[error("screenshot capture failed: {0}")]
44 Screenshot(String),
45
46 #[error(transparent)]
48 Extract(#[from] crate::extract::ExtractError),
49
50 #[error(transparent)]
52 Schema(#[from] crate::schema::SchemaError),
53
54 #[error(transparent)]
56 Io(#[from] std::io::Error),
57
58 #[error("invalid glob pattern: {0}")]
60 InvalidGlob(#[from] globset::Error),
61}
62
63impl Error {
64 #[must_use]
66 pub fn is_timeout(&self) -> bool {
67 matches!(self, Self::Timeout { .. })
68 }
69
70 #[must_use]
72 pub fn is_network(&self) -> bool {
73 matches!(self, Self::Timeout { .. } | Self::AddressNotAllowed(_))
74 }
75
76 #[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#[derive(Debug)]
96pub(crate) enum UrlError {
97 Invalid(String),
99 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}