1use std::{
4 error::Error,
5 fmt::{Debug, Display},
6};
7
8#[cfg(feature = "unstable-internals")]
9use strum::EnumIter;
10#[cfg(feature = "unstable-internals")]
11use strum::IntoEnumIterator;
12
13#[cfg_attr(feature = "unstable-internals", derive(EnumIter))]
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum FaithErrorKind {
19 Aborted,
21 AddressParse,
23 BodyStream,
25 Closed,
27 Config,
29 ContentLengthOverrun,
31 FileExists,
33 FileWrite,
35 IntegrityMismatch,
37 InvalidCompression,
39 InvalidHeader,
41 InvalidIntegrity,
43 InvalidMethod,
45 InvalidPath,
47 InvalidUrl,
49 JsonParse,
51 MissingContentType,
53 Network,
55 PemParse,
57 Redirect,
59 ResponseAlreadyDisturbed,
61 ResponseBodyNull,
63 Timeout,
65}
66
67impl FaithErrorKind {
68 #[cfg(feature = "unstable-internals")]
70 #[cfg_attr(docsrs, doc(cfg(feature = "unstable-internals")))]
71 pub fn code(self) -> String {
72 format!("{self:?}")
73 }
74
75 pub(crate) fn default_message(self) -> &'static str {
76 match self {
77 Self::Aborted => "the request was aborted",
78 Self::AddressParse => "invalid IP address and/or port",
79 Self::BodyStream => "internal response body stream copy error",
80 Self::Closed => "the agent has been closed",
81 Self::Config => "invalid agent configuration",
82 Self::ContentLengthOverrun => "response body exceeded the advertised Content-Length",
83 Self::FileExists => "the destination file already exists",
84 Self::FileWrite => "could not write the destination file",
85 Self::IntegrityMismatch => "resource integrity check failed",
86 Self::InvalidCompression => "invalid request body compression",
87 Self::InvalidHeader => "invalid header name or value",
88 Self::InvalidIntegrity => "invalid integrity value",
89 Self::InvalidMethod => "invalid HTTP method",
90 Self::InvalidPath => "destination does not name a local path",
91 Self::InvalidUrl => "invalid URL",
92 Self::JsonParse => "invalid json in response body",
93 Self::MissingContentType => "a QUERY request with a body requires a Content-Type",
94 Self::Network => "network error",
95 Self::PemParse => "invalid client certificate or key",
96 Self::Redirect => "got a redirect",
97 Self::ResponseAlreadyDisturbed => "response body already disturbed",
98 Self::ResponseBodyNull => "response cannot carry a body to write",
99 Self::Timeout => "timed out",
100 }
101 }
102}
103
104#[cfg(feature = "unstable-internals")]
106pub fn error_codes() -> Vec<String> {
107 FaithErrorKind::iter().map(FaithErrorKind::code).collect()
108}
109
110#[derive(Debug, Clone)]
114pub struct FaithError {
115 kind: FaithErrorKind,
116 message: Option<String>,
118}
119
120impl FaithError {
121 pub fn new(kind: FaithErrorKind, message: impl Into<String>) -> Self {
125 Self {
126 kind,
127 message: Some(message.into()),
128 }
129 }
130
131 pub fn kind(&self) -> FaithErrorKind {
133 self.kind
134 }
135}
136
137impl From<FaithErrorKind> for FaithError {
138 fn from(kind: FaithErrorKind) -> Self {
139 Self {
140 kind,
141 message: None,
142 }
143 }
144}
145
146fn faith_kind_in_chain(err: &(dyn Error + 'static)) -> Option<FaithErrorKind> {
154 let mut source = err.source();
155 while let Some(e) = source {
156 if let Some(faith) = e.downcast_ref::<FaithError>() {
157 return Some(faith.kind());
158 }
159 source = e.source();
160 }
161
162 None
163}
164
165impl From<std::convert::Infallible> for FaithError {
168 fn from(never: std::convert::Infallible) -> Self {
169 match never {}
170 }
171}
172
173impl From<reqwest::Error> for FaithError {
174 fn from(err: reqwest::Error) -> Self {
175 let mut msg = format!("{err:?}");
177 let mut source = err.source();
178 while let Some(e) = source {
179 msg.push_str(&format!(" -> {e:?}"));
180 source = e.source();
181 }
182
183 if err.is_timeout() {
184 return FaithError::new(FaithErrorKind::Timeout, msg);
185 }
186
187 let kind = err
190 .is_redirect()
191 .then(|| faith_kind_in_chain(&err))
192 .flatten()
193 .unwrap_or(FaithErrorKind::Network);
194
195 FaithError::new(kind, msg)
196 }
197}
198
199impl From<reqwest_middleware::Error> for FaithError {
200 fn from(err: reqwest_middleware::Error) -> Self {
201 match err {
202 reqwest_middleware::Error::Middleware(err) => {
203 FaithError::new(FaithErrorKind::Network, err.to_string())
204 }
205 reqwest_middleware::Error::Reqwest(err) => err.into(),
206 }
207 }
208}
209
210impl Error for FaithError {}
211
212impl Display for FaithError {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 write!(
215 f,
216 "{:?}: {}",
217 self.kind,
218 self.message
219 .as_deref()
220 .unwrap_or_else(|| self.kind.default_message())
221 )
222 }
223}
224
225#[cfg(all(test, feature = "unstable-internals"))]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn every_code_is_distinct_and_named() {
231 let codes = error_codes();
232 let unique: std::collections::BTreeSet<_> = codes.iter().collect();
233 assert_eq!(unique.len(), codes.len(), "two kinds report the same code");
234 assert!(codes.iter().all(|code| !code.is_empty()));
235 }
236
237 #[test]
238 fn a_message_is_prefixed_with_the_code_it_reports() {
239 for kind in FaithErrorKind::iter() {
240 let code = kind.code();
241 let rendered = FaithError::from(kind).to_string();
242 assert!(
243 rendered.starts_with(&format!("{code}: ")),
244 "{rendered} does not lead with {code}"
245 );
246 }
247 }
248
249 #[test]
250 fn a_kind_without_a_message_falls_back_to_its_own() {
251 let err = FaithError::from(FaithErrorKind::Closed);
252 assert_eq!(err.to_string(), "Closed: the agent has been closed");
253
254 let err = FaithError::new(FaithErrorKind::Closed, "gone");
255 assert_eq!(err.to_string(), "Closed: gone");
256 }
257}