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}