1use std::error::Error as StdError;
4use std::time::Duration;
5
6pub type Result<T> = std::result::Result<T, Error>;
8
9pub(crate) type BoxError = Box<dyn StdError + Send + Sync + 'static>;
11
12#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum Error {
16 #[error("invalid URL '{url}': {reason}")]
18 InvalidUrl {
19 url: String,
21 reason: String,
23 },
24
25 #[error("page load timed out after {}s at {url}", timeout.as_secs())]
27 Timeout {
28 url: String,
30 timeout: Duration,
32 },
33
34 #[error("address not allowed: {host}")]
36 AddressNotAllowed {
37 host: String,
39 },
40
41 #[error("engine error: {source}")]
43 Engine {
44 url: Option<String>,
46 #[source]
48 source: BoxError,
49 },
50
51 #[error("JavaScript evaluation failed: {source}")]
53 JavaScript {
54 url: Option<String>,
56 #[source]
58 source: BoxError,
59 },
60
61 #[error("screenshot capture failed: {source}")]
63 Screenshot {
64 url: Option<String>,
66 #[source]
68 source: BoxError,
69 },
70
71 #[error(transparent)]
73 Extract(#[from] crate::extract::ExtractError),
74
75 #[error(transparent)]
77 Schema(#[from] crate::schema::SchemaError),
78
79 #[error(transparent)]
81 Io(#[from] std::io::Error),
82
83 #[error(transparent)]
85 InvalidGlob(#[from] globset::Error),
86}
87
88impl Error {
89 pub(crate) fn engine(source: impl Into<BoxError>, url: Option<String>) -> Self {
91 Self::Engine {
92 url,
93 source: source.into(),
94 }
95 }
96
97 pub(crate) fn screenshot(source: impl Into<BoxError>, url: Option<String>) -> Self {
99 Self::Screenshot {
100 url,
101 source: source.into(),
102 }
103 }
104
105 pub(crate) fn javascript(source: impl Into<BoxError>, url: Option<String>) -> Self {
107 Self::JavaScript {
108 url,
109 source: source.into(),
110 }
111 }
112
113 #[must_use]
115 pub fn is_timeout(&self) -> bool {
116 matches!(self, Self::Timeout { .. })
117 }
118
119 #[must_use]
121 pub fn is_network(&self) -> bool {
122 matches!(self, Self::Timeout { .. } | Self::AddressNotAllowed { .. })
123 }
124
125 #[must_use]
127 pub fn url(&self) -> Option<&str> {
128 match self {
129 Self::InvalidUrl { url, .. } | Self::Timeout { url, .. } => Some(url),
130 Self::Engine { url, .. } | Self::JavaScript { url, .. } | Self::Screenshot { url, .. } => url.as_deref(),
131 _ => None,
132 }
133 }
134
135 #[must_use]
137 pub fn host(&self) -> Option<&str> {
138 match self {
139 Self::AddressNotAllowed { host } => Some(host),
140 _ => None,
141 }
142 }
143}
144
145#[derive(Debug)]
146pub(crate) enum UrlError {
147 Invalid(String),
148 PrivateAddress(String),
149}
150
151pub(crate) fn map_url_error(url: &str, e: UrlError) -> Error {
152 match e {
153 UrlError::PrivateAddress(host) => Error::AddressNotAllowed { host },
154 UrlError::Invalid(reason) => Error::InvalidUrl {
155 url: url.into(),
156 reason,
157 },
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn assert_send_sync() {
167 fn check<T: Send + Sync>() {}
168 check::<Error>();
169 }
170
171 #[test]
172 fn timeout_predicates() {
173 let err = Error::Timeout {
174 url: "https://example.com".into(),
175 timeout: Duration::from_secs(30),
176 };
177 assert!(err.is_timeout());
178 assert!(err.is_network());
179 assert_eq!(err.url(), Some("https://example.com"));
180 assert_eq!(err.host(), None);
181 }
182
183 #[test]
184 fn address_not_allowed_predicates() {
185 let err = Error::AddressNotAllowed {
186 host: "127.0.0.1".into(),
187 };
188 assert!(!err.is_timeout());
189 assert!(err.is_network());
190 assert_eq!(err.url(), None);
191 assert_eq!(err.host(), Some("127.0.0.1"));
192 }
193
194 #[test]
195 fn invalid_url_carries_url() {
196 let err = Error::InvalidUrl {
197 url: "bad://url".into(),
198 reason: "scheme not allowed".into(),
199 };
200 assert!(!err.is_network());
201 assert_eq!(err.url(), Some("bad://url"));
202 assert_eq!(err.host(), None);
203 }
204
205 #[test]
206 fn engine_helper_preserves_source_chain() {
207 let inner = std::io::Error::other("disk full");
208 let err = Error::engine(inner, Some("https://example.com".into()));
209 assert_eq!(err.url(), Some("https://example.com"));
210 assert!(err.source().is_some());
211 assert_eq!(err.to_string(), "engine error: disk full");
212 }
213
214 #[test]
215 fn engine_without_url_returns_none() {
216 let err = Error::engine(std::io::Error::other("crash"), None);
217 assert!(err.url().is_none());
218 }
219}