Skip to main content

hclient_core/
error.rs

1use std::error::Error as StdError;
2use std::fmt::Display;
3use std::sync::Arc;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum Phase {
8    /// Name resolution alone, which is a phase a caller can distinguish
9    /// and a connector mostly cannot — see
10    /// [`Timeouts::resolve`](crate::Timeouts::resolve) for what it bounds
11    /// and why it is not
12    /// [`Connect`](Self::Connect) minus the rest.
13    Resolve,
14    Connect,
15    FirstByte,
16    BetweenBytes,
17    Total,
18}
19
20/// The error's category. Exists so the consumer doesn't have to classify
21/// errors by substring-matching on `Display`.
22#[non_exhaustive]
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum ErrorKind {
25    Resolve,
26    Connect,
27    Tls,
28    Redirect,
29    Timeout(Phase),
30    Body,
31    Decode,
32    Status,
33    Unsupported,
34    /// The capability behind the failed operation was pulled out from under
35    /// it before it could finish — typically, the runtime is shutting down
36    /// while the task was still queued (see `hclient_rt::Cancelled`,
37    /// returned by `Blocking::run`, `amendment-C5`).
38    ///
39    /// A separate variant, not `Other`: `Other` is the honest answer for a
40    /// GENUINELY opaque backend error (the default `Transport::to_error`,
41    /// when the backend has nothing to say about the category). Cancellation
42    /// is the opposite of opacity: it's a condition known in advance and
43    /// already typed (`Cancelled` is not a string or an OS error code, but a
44    /// concrete type), one that EVERY future consumer of the `Blocking`
45    /// capability will hit, not a one-off for a single backend. It must not
46    /// be mixed with `Other`, and even less with the category of the failed
47    /// operation itself (e.g. `Resolve` for a DNS resolver built on
48    /// `Blocking`, see `hclient-dns-system`) — for the same reason `Resolve`
49    /// and `Other` aren't mixed with each other: the caller must be able to
50    /// tell "this attempt failed on its merits" from "this attempt didn't
51    /// finish because the runtime is shutting down" without a downcast —
52    /// just by comparing `kind()`.
53    Cancelled,
54    Other,
55}
56
57/// `Clone` is deliberate: reqwest's opaque, unclonable error is a source of
58/// constant complaints (reqwest#1053).
59///
60/// `source` must be `Send + Sync` — the one documented exception to the
61/// crate invariant "no declared `Send`/`Sync` bound anywhere." Without this
62/// bound, `Arc<dyn Error>` erases the source's auto-traits, and `Error`
63/// (and with it the future `Client::execute` returns) would be `!Send` for
64/// every transport — `tokio::spawn(client.get(u).send())` would never
65/// compile. All three v0.1 backends (hyper, wasi:http, browser fetch
66/// without `target_feature = "atomics"`) already produce `Send + Sync`
67/// errors, so this pins down a fact rather than adding a new restriction;
68/// a transport with a fundamentally `!Send` error won't be able to use
69/// this wrapper.
70#[derive(Debug, Clone)]
71pub struct Error {
72    kind: ErrorKind,
73    source: Arc<dyn StdError + Send + Sync + 'static>, // send-bound-exception: amendment-C1
74}
75
76impl Error {
77    pub fn new<E>(kind: ErrorKind, source: E) -> Self
78    where
79        E: StdError + Send + Sync + 'static, // send-bound-exception: amendment-C1
80    {
81        Self {
82            kind,
83            source: Arc::new(source),
84        }
85    }
86    pub fn kind(&self) -> &ErrorKind {
87        &self.kind
88    }
89    pub fn is_timeout(&self) -> bool {
90        matches!(self.kind, ErrorKind::Timeout(_))
91    }
92    pub fn is_redirect(&self) -> bool {
93        matches!(self.kind, ErrorKind::Redirect)
94    }
95    pub fn is_connect(&self) -> bool {
96        matches!(self.kind, ErrorKind::Connect)
97    }
98    pub fn is_unsupported(&self) -> bool {
99        matches!(self.kind, ErrorKind::Unsupported)
100    }
101    pub fn is_cancelled(&self) -> bool {
102        matches!(self.kind, ErrorKind::Cancelled)
103    }
104}
105
106impl Display for Error {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "{:?}: {}", self.kind, self.source)
109    }
110}
111
112impl StdError for Error {
113    fn source(&self) -> Option<&(dyn StdError + 'static)> {
114        Some(&*self.source)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use std::error::Error as StdError;
122    use std::fmt::Display;
123
124    #[derive(Debug)]
125    struct Src;
126    impl Display for Src {
127        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128            write!(f, "boom")
129        }
130    }
131    impl StdError for Src {}
132
133    #[test]
134    fn preserves_kind_and_source_without_stringifying() {
135        let e = Error::new(ErrorKind::Resolve, Src);
136        assert_eq!(e.kind(), &ErrorKind::Resolve);
137        // The source is available whole — not as a substring of the message.
138        let src = StdError::source(&e).unwrap();
139        assert!(src.downcast_ref::<Src>().is_some());
140    }
141
142    #[test]
143    fn is_clone_which_reqwest_error_is_not() {
144        let e = Error::new(ErrorKind::Connect, Src);
145        let c = e.clone();
146        assert_eq!(c.kind(), &ErrorKind::Connect);
147        // The clone must share the same source, not copy or lose it: the
148        // source pointers of the original and the clone must match.
149        let a = StdError::source(&e).unwrap() as *const dyn StdError;
150        let b = StdError::source(&c).unwrap() as *const dyn StdError;
151        assert!(std::ptr::eq(a, b));
152    }
153
154    #[test]
155    fn predicates_agree_with_kind() {
156        assert!(Error::new(ErrorKind::Timeout(Phase::Connect), Src).is_timeout());
157        assert!(Error::new(ErrorKind::Redirect, Src).is_redirect());
158        assert!(Error::new(ErrorKind::Connect, Src).is_connect());
159        assert!(!Error::new(ErrorKind::Body, Src).is_connect());
160        assert!(Error::new(ErrorKind::Unsupported, Src).is_unsupported());
161        assert!(!Error::new(ErrorKind::Body, Src).is_unsupported());
162        assert!(Error::new(ErrorKind::Cancelled, Src).is_cancelled());
163        // Cancellation is neither a DNS failure nor an opaque "other"
164        // error: both checks are needed — either alone would be
165        // insufficient to catch a regression that confused `Cancelled`
166        // with either of these two neighbors.
167        assert!(!Error::new(ErrorKind::Resolve, Src).is_cancelled());
168        assert!(!Error::new(ErrorKind::Other, Src).is_cancelled());
169    }
170
171    // `Error: Send + Sync` (amendment-C1) is asserted in
172    // `crates/hclient-core/tests/shape.rs`, not here: a bare
173    // `fn _assert<T: Send + Sync>() {}` inside `src` is exactly what the
174    // `no-declared-send` guard's pattern matches, so the assertion would
175    // need a `send-bound-exception` marker of its own. Outside `src` it
176    // needs none, which keeps the guard's blind spot as small as the two
177    // lines that genuinely are exceptions.
178}