1use std::fmt;
8
9use bytes::Bytes;
10use http::StatusCode;
11use thiserror::Error;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum TransportKind {
16 Connect,
18 Body,
20 Decode,
22 Redirect,
24 Request,
26 Builder,
28 Upgrade,
30 Other,
32}
33
34impl fmt::Display for TransportKind {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::Connect => write!(f, "connect"),
38 Self::Body => write!(f, "body"),
39 Self::Decode => write!(f, "decode"),
40 Self::Redirect => write!(f, "redirect"),
41 Self::Request => write!(f, "request"),
42 Self::Builder => write!(f, "builder"),
43 Self::Upgrade => write!(f, "upgrade"),
44 Self::Other => write!(f, "other"),
45 }
46 }
47}
48
49#[derive(Debug, Error, Clone)]
51#[must_use = "errors must be handled or propagated with `?`"]
52pub enum Error {
53 #[error("invalid base URL: {0}")]
55 InvalidBaseUrl(#[from] url::ParseError),
56
57 #[error("transport error ({kind}): {message}")]
59 Transport {
60 kind: TransportKind,
62 message: String,
64 },
65
66 #[error("HTTP {status} {status_text}: {message}")]
68 Http {
69 status: StatusCode,
71 status_text: String,
73 message: String,
75 body: Option<Bytes>,
77 },
78
79 #[cfg(feature = "json")]
81 #[error("failed to deserialize response body: {message}")]
82 Deserialize {
83 status: StatusCode,
84 message: String,
85 body: Option<Bytes>,
86 },
87
88 #[cfg(feature = "validate")]
90 #[error("response validation failed: {message}")]
91 Validation {
92 status: StatusCode,
93 message: String,
94 body: Option<Bytes>,
95 },
96
97 #[error("request timed out")]
99 Timeout,
100
101 #[error("request was cancelled")]
103 Cancelled,
104
105 #[error("response body exceeded limit of {limit} bytes")]
108 BodyTooLarge {
109 limit: u64,
111 },
112
113 #[error("client base URL is required; call ClientBuilder::base_url")]
115 MissingBaseUrl,
116
117 #[error("retries exhausted after {attempts} attempts")]
119 RetryExhausted {
120 attempts: u32,
122 last: Option<Box<Error>>,
124 },
125
126 #[error("hook error: {0}")]
130 Hook(String),
131
132 #[error("{0}")]
134 Other(String),
135}
136
137impl Error {
138 pub fn transport(kind: TransportKind, message: impl Into<String>) -> Self {
140 Self::Transport {
141 kind,
142 message: message.into(),
143 }
144 }
145
146 pub fn transport_message(message: impl Into<String>) -> Self {
148 Self::transport(TransportKind::Other, message)
149 }
150
151 pub fn transport_kind(&self) -> Option<TransportKind> {
153 match self {
154 Self::Transport { kind, .. } => Some(*kind),
155 _ => None,
156 }
157 }
158
159 pub fn transport_detail(&self) -> Option<&str> {
161 match self {
162 Self::Transport { message, .. } => Some(message),
163 _ => None,
164 }
165 }
166
167 pub fn is_transport(&self) -> bool {
169 matches!(self, Self::Transport { .. })
170 }
171
172 pub fn is_timeout(&self) -> bool {
174 matches!(self, Self::Timeout)
175 }
176
177 pub fn http(status: StatusCode, message: impl Into<String>, body: Option<Bytes>) -> Self {
179 Self::http_with_status_text(
180 status,
181 status.canonical_reason().unwrap_or("").to_string(),
182 message,
183 body,
184 )
185 }
186
187 pub fn http_with_status_text(
189 status: StatusCode,
190 status_text: impl Into<String>,
191 message: impl Into<String>,
192 body: Option<Bytes>,
193 ) -> Self {
194 Self::Http {
195 status,
196 status_text: status_text.into(),
197 message: message.into(),
198 body,
199 }
200 }
201
202 pub fn status(&self) -> Option<StatusCode> {
204 match self {
205 Self::Http { status, .. } => Some(*status),
206 #[cfg(feature = "json")]
207 Self::Deserialize { status, .. } => Some(*status),
208 #[cfg(feature = "validate")]
209 Self::Validation { status, .. } => Some(*status),
210 _ => None,
211 }
212 }
213
214 pub fn status_text(&self) -> Option<&str> {
216 match self {
217 Self::Http { status_text, .. } => Some(status_text),
218 _ => None,
219 }
220 }
221
222 pub fn body(&self) -> Option<&Bytes> {
224 match self {
225 Self::Http { body, .. } => body.as_ref(),
226 #[cfg(feature = "json")]
227 Self::Deserialize { body, .. } => body.as_ref(),
228 #[cfg(feature = "validate")]
229 Self::Validation { body, .. } => body.as_ref(),
230 _ => None,
231 }
232 }
233
234 pub fn is_retry_exhausted(&self) -> bool {
236 matches!(self, Self::RetryExhausted { .. })
237 }
238
239 pub fn retry_exhausted_last(&self) -> Option<&Error> {
241 match self {
242 Self::RetryExhausted { last, .. } => last.as_deref(),
243 _ => None,
244 }
245 }
246
247 pub fn is_cancelled(&self) -> bool {
249 matches!(self, Self::Cancelled)
250 }
251
252 pub fn is_body_too_large(&self) -> bool {
254 matches!(self, Self::BodyTooLarge { .. })
255 }
256
257 pub fn body_too_large_limit(&self) -> Option<u64> {
259 match self {
260 Self::BodyTooLarge { limit } => Some(*limit),
261 _ => None,
262 }
263 }
264
265 pub fn hook(msg: impl Into<String>) -> Self {
268 Self::Hook(msg.into())
269 }
270
271 pub fn is_hook(&self) -> bool {
273 matches!(self, Self::Hook(_))
274 }
275
276 pub(crate) fn retry_exhausted(attempts: u32, last: Error) -> Self {
277 Self::RetryExhausted {
278 attempts,
279 last: Some(Box::new(last)),
280 }
281 }
282
283 #[cfg(feature = "json")]
307 pub fn api_json<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
308 let body = self.body()?;
309 serde_json::from_slice(body).ok()
310 }
311
312 #[cfg(feature = "validate")]
314 pub fn api_json_validated<T>(&self) -> Option<T>
315 where
316 T: serde::de::DeserializeOwned + garde::Validate,
317 T::Context: Default,
318 {
319 let body = self.body()?;
320 let value: T = serde_json::from_slice(body).ok()?;
321 value.validate().ok()?;
322 Some(value)
323 }
324}
325
326pub(crate) fn map_transport_error(err: reqwest::Error) -> Error {
327 if err.is_timeout() {
328 return Error::Timeout;
329 }
330
331 let kind = transport_kind_from_reqwest(&err);
332 Error::Transport {
333 kind,
334 message: err.to_string(),
335 }
336}
337
338fn transport_kind_from_reqwest(err: &reqwest::Error) -> TransportKind {
339 #[cfg(not(target_arch = "wasm32"))]
340 if err.is_connect() {
341 return TransportKind::Connect;
342 }
343
344 if err.is_body() {
345 TransportKind::Body
346 } else if err.is_decode() {
347 TransportKind::Decode
348 } else if err.is_redirect() {
349 TransportKind::Redirect
350 } else if err.is_request() {
351 TransportKind::Request
352 } else if err.is_builder() {
353 TransportKind::Builder
354 } else if err.is_upgrade() {
355 TransportKind::Upgrade
356 } else {
357 TransportKind::Other
358 }
359}
360
361#[cfg(all(test, feature = "json"))]
362mod tests {
363 use super::*;
364 use serde::Deserialize;
365
366 #[derive(Debug, Deserialize, PartialEq)]
367 struct ApiError {
368 message: String,
369 }
370
371 #[test]
372 fn api_json_parses_http_body() {
373 let err = Error::http_with_status_text(
374 StatusCode::BAD_REQUEST,
375 "Bad Request",
376 "bad request",
377 Some(bytes::Bytes::from_static(br#"{"message":"invalid"}"#)),
378 );
379 let api: ApiError = err.api_json().unwrap();
380 assert_eq!(api.message, "invalid");
381 }
382
383 #[test]
384 fn status_and_status_text_accessors() {
385 let err = Error::http(StatusCode::NOT_FOUND, "not found", None);
386 assert_eq!(err.status(), Some(StatusCode::NOT_FOUND));
387 assert_eq!(err.status_text(), Some("Not Found"));
388 }
389
390 #[test]
391 fn api_json_returns_none_without_body() {
392 let err = Error::http(StatusCode::INTERNAL_SERVER_ERROR, "err", None);
393 assert!(err.api_json::<ApiError>().is_none());
394 }
395
396 #[test]
397 fn hook_constructor_and_is_hook() {
398 let err = Error::hook("blocked");
399 assert!(err.is_hook());
400 assert!(matches!(err, Error::Hook(msg) if msg == "blocked"));
401 }
402
403 #[test]
404 fn retry_exhausted_helper_sets_flag() {
405 let err = Error::retry_exhausted(3, Error::Timeout);
406 assert!(err.is_retry_exhausted());
407 assert!(matches!(
408 err,
409 Error::RetryExhausted {
410 attempts: 3,
411 last: Some(_)
412 }
413 ));
414 assert!(matches!(err.retry_exhausted_last(), Some(Error::Timeout)));
415 }
416
417 #[test]
418 fn transport_helpers() {
419 let err = Error::transport(TransportKind::Connect, "connection refused");
420 assert!(err.is_transport());
421 assert_eq!(err.transport_kind(), Some(TransportKind::Connect));
422 assert_eq!(err.transport_detail(), Some("connection refused"));
423 }
424
425 #[test]
426 fn transport_message_defaults_to_other() {
427 let err = Error::transport_message("tower layer failed");
428 assert_eq!(err.transport_kind(), Some(TransportKind::Other));
429 }
430}