Skip to main content

spire_core/
error.rs

1//! Error and result types.
2//!
3//! An [`Error`] carries a classified [`ErrorKind`], a message and an optional
4//! source. The kind drives retry decisions: the crawler retries a failed
5//! request when [`Error::is_retryable`] holds.
6
7use std::convert::Infallible;
8use std::fmt;
9use std::time::Duration;
10
11use ::http::{StatusCode, Uri};
12
13/// A result whose error defaults to [`Error`].
14pub type Result<T, E = Error> = std::result::Result<T, E>;
15
16/// A boxed, thread-safe source error.
17pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
18
19/// Classification of a failure, so callers can branch without matching on
20/// messages.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum ErrorKind {
24    /// The input was invalid: a malformed URL, request or configuration.
25    Invalid,
26    /// The transfer failed, or the remote answered with a status worth
27    /// retrying, such as `429` or `503`.
28    Http,
29    /// The remote refused the request in a way retrying won't fix, such as
30    /// `404` or `403`.
31    Rejected,
32    /// The operation timed out.
33    Timeout,
34    /// A backend failed, e.g. while connecting or driving a browser.
35    Backend,
36    /// A worker or handler failed.
37    Worker,
38    /// Reading from or writing to a dataset failed.
39    Dataset,
40    /// Reading from or writing to the frontier failed.
41    Frontier,
42    /// The operation was cancelled.
43    Cancelled,
44}
45
46impl ErrorKind {
47    /// Returns the kind as a lowercase string.
48    #[must_use]
49    pub const fn as_str(self) -> &'static str {
50        match self {
51            Self::Invalid => "invalid",
52            Self::Http => "http",
53            Self::Rejected => "rejected",
54            Self::Timeout => "timeout",
55            Self::Backend => "backend",
56            Self::Worker => "worker",
57            Self::Dataset => "dataset",
58            Self::Frontier => "frontier",
59            Self::Cancelled => "cancelled",
60        }
61    }
62
63    /// Whether an operation failing with this kind is worth retrying. Only
64    /// transient failures are retryable.
65    #[must_use]
66    pub const fn is_retryable(self) -> bool {
67        matches!(self, Self::Http | Self::Timeout | Self::Backend)
68    }
69}
70
71impl fmt::Display for ErrorKind {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.write_str(self.as_str())
74    }
75}
76
77/// An error carrying a classified [`ErrorKind`], a message, and an optional
78/// source.
79#[derive(Debug, thiserror::Error)]
80pub struct Error {
81    kind: ErrorKind,
82    message: String,
83    status: Option<StatusCode>,
84    retry_after: Option<Duration>,
85    uri: Option<Box<Uri>>,
86    #[source]
87    source: Option<BoxError>,
88}
89
90impl Error {
91    /// Creates an error of the given [`ErrorKind`] with an empty message.
92    #[must_use]
93    pub fn new(kind: ErrorKind) -> Self {
94        Self {
95            kind,
96            message: String::new(),
97            status: None,
98            retry_after: None,
99            uri: None,
100            source: None,
101        }
102    }
103
104    /// Wraps an arbitrary error under the given [`ErrorKind`].
105    ///
106    /// If the error already is an [`Error`], it is returned unchanged and
107    /// keeps its own kind.
108    pub fn from_boxed(kind: ErrorKind, error: impl Into<BoxError>) -> Self {
109        match error.into().downcast::<Self>() {
110            Ok(error) => *error,
111            Err(error) => Self::new(kind).with_source(error),
112        }
113    }
114
115    /// Creates an [`Invalid`](ErrorKind::Invalid) error with a message.
116    pub fn invalid(message: impl Into<String>) -> Self {
117        Self::new(ErrorKind::Invalid).with_message(message)
118    }
119
120    /// Creates a [`Backend`](ErrorKind::Backend) error with a message.
121    pub fn backend(message: impl Into<String>) -> Self {
122        Self::new(ErrorKind::Backend).with_message(message)
123    }
124
125    /// Creates a [`Worker`](ErrorKind::Worker) error with a message.
126    pub fn worker(message: impl Into<String>) -> Self {
127        Self::new(ErrorKind::Worker).with_message(message)
128    }
129
130    /// Replaces the message.
131    #[must_use]
132    pub fn with_message(mut self, message: impl Into<String>) -> Self {
133        self.message = message.into();
134        self
135    }
136
137    /// Attaches a source error.
138    #[must_use]
139    pub fn with_source(mut self, source: impl Into<BoxError>) -> Self {
140        self.source = Some(source.into());
141        self
142    }
143
144    /// Attaches the response status that caused the error.
145    #[must_use]
146    pub const fn with_status(mut self, status: StatusCode) -> Self {
147        self.status = Some(status);
148        self
149    }
150
151    /// Attaches the URI of the request that failed.
152    #[must_use]
153    pub fn with_uri(mut self, uri: Uri) -> Self {
154        self.uri = Some(Box::new(uri));
155        self
156    }
157
158    /// Asks for the failed request's tag to be paused for `delay` before it
159    /// is retried, e.g. from a `Retry-After` header.
160    #[must_use]
161    pub const fn with_retry_after(mut self, delay: Duration) -> Self {
162        self.retry_after = Some(delay);
163        self
164    }
165
166    /// Returns the [`ErrorKind`].
167    #[must_use]
168    pub const fn kind(&self) -> ErrorKind {
169        self.kind
170    }
171
172    /// Returns the message, which may be empty.
173    #[must_use]
174    pub fn message(&self) -> &str {
175        &self.message
176    }
177
178    /// Returns the response status that caused the error, if any.
179    #[must_use]
180    pub const fn status(&self) -> Option<StatusCode> {
181        self.status
182    }
183
184    /// Returns the URI of the request that failed, if known.
185    #[must_use]
186    pub fn uri(&self) -> Option<&Uri> {
187        self.uri.as_deref()
188    }
189
190    /// Returns how long to wait before retrying, if the remote said so.
191    #[must_use]
192    pub const fn retry_after(&self) -> Option<Duration> {
193        self.retry_after
194    }
195
196    /// Whether the failed operation is worth retrying.
197    #[must_use]
198    pub const fn is_retryable(&self) -> bool {
199        self.kind.is_retryable()
200    }
201}
202
203impl fmt::Display for Error {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match (self.message.is_empty(), &self.source) {
206            (false, _) => f.write_str(&self.message),
207            (true, Some(source)) => write!(f, "{}: {source}", self.kind),
208            (true, None) => write!(f, "{} error", self.kind),
209        }
210    }
211}
212
213impl From<Infallible> for Error {
214    fn from(never: Infallible) -> Self {
215        match never {}
216    }
217}
218
219impl From<::http::Error> for Error {
220    fn from(error: ::http::Error) -> Self {
221        Self::invalid("invalid request").with_source(error)
222    }
223}
224
225impl From<::http::uri::InvalidUri> for Error {
226    fn from(error: ::http::uri::InvalidUri) -> Self {
227        Self::invalid("invalid uri").with_source(error)
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use std::error::Error as _;
234
235    use super::*;
236
237    #[test]
238    fn display_prefers_message() {
239        let error = Error::backend("pool exhausted");
240        assert_eq!(error.to_string(), "pool exhausted");
241        assert_eq!(Error::new(ErrorKind::Timeout).to_string(), "timeout error");
242    }
243
244    #[test]
245    fn from_boxed_keeps_existing_kind() {
246        let inner = Error::new(ErrorKind::Timeout);
247        let error = Error::from_boxed(ErrorKind::Backend, inner);
248        assert_eq!(error.kind(), ErrorKind::Timeout);
249
250        let error = Error::from_boxed(ErrorKind::Backend, std::io::Error::other("io"));
251        assert_eq!(error.kind(), ErrorKind::Backend);
252        assert!(error.source().is_some());
253    }
254
255    #[test]
256    fn retryable_kinds() {
257        assert!(Error::new(ErrorKind::Http).is_retryable());
258        assert!(!Error::new(ErrorKind::Rejected).is_retryable());
259        assert!(!Error::invalid("bad").is_retryable());
260    }
261}