1use std::fmt;
8use std::sync::Arc;
9
10use bytes::Bytes;
11use http::StatusCode;
12use thiserror::Error;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum TransportKind {
17 Connect,
19 Body,
21 Decode,
23 Redirect,
25 Request,
27 Builder,
29 Upgrade,
31 Other,
33}
34
35impl fmt::Display for TransportKind {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::Connect => write!(f, "connect"),
39 Self::Body => write!(f, "body"),
40 Self::Decode => write!(f, "decode"),
41 Self::Redirect => write!(f, "redirect"),
42 Self::Request => write!(f, "request"),
43 Self::Builder => write!(f, "builder"),
44 Self::Upgrade => write!(f, "upgrade"),
45 Self::Other => write!(f, "other"),
46 }
47 }
48}
49
50#[derive(Debug, Error, Clone)]
52#[must_use = "errors must be handled or propagated with `?`"]
53pub enum Error {
54 #[error("invalid base URL: {0}")]
56 InvalidBaseUrl(#[from] url::ParseError),
57
58 #[error("transport error ({kind}): {message}")]
60 Transport {
61 kind: TransportKind,
63 message: String,
65 #[source]
67 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
68 },
69
70 #[error("HTTP {status} {status_text}: {message}")]
72 Http {
73 status: StatusCode,
75 status_text: String,
77 message: String,
79 body: Option<Bytes>,
81 },
82
83 #[cfg(feature = "json")]
85 #[error("failed to deserialize response body: {message}")]
86 Deserialize {
87 status: StatusCode,
88 message: String,
89 body: Option<Bytes>,
90 },
91
92 #[cfg(feature = "validate")]
94 #[error("response validation failed: {message}")]
95 Validation {
96 status: StatusCode,
97 message: String,
98 body: Option<Bytes>,
99 },
100
101 #[error("request timed out")]
103 Timeout,
104
105 #[error("request was cancelled")]
107 Cancelled,
108
109 #[error("response body exceeded limit of {limit} bytes")]
112 BodyTooLarge {
113 limit: u64,
115 },
116
117 #[error("client base URL is required; call ClientBuilder::base_url")]
119 MissingBaseUrl,
120
121 #[error("retries exhausted after {attempts} attempts")]
123 RetryExhausted {
124 attempts: u32,
126 last: Option<Box<Error>>,
128 },
129
130 #[error("hook error: {0}")]
134 Hook(String),
135
136 #[error("failed to serialize query: {0}")]
141 QuerySerialize(String),
142
143 #[error("invalid header name: {0}")]
145 InvalidHeaderName(String),
146
147 #[error("invalid header value: {0}")]
149 InvalidHeaderValue(String),
150
151 #[error("missing path parameter for `{0}`")]
153 MissingPathParam(String),
154
155 #[error("route not in schema registry: {method} {path}")]
157 SchemaRoute {
158 method: String,
160 path: String,
162 },
163
164 #[cfg(feature = "schema-validate")]
166 #[error("JSON schema validation failed ({phase}): {message}")]
167 SchemaValidation {
168 phase: &'static str,
170 message: String,
172 },
173
174 #[error("invalid authorization header: {0}")]
176 InvalidAuthHeader(String),
177
178 #[error("automatic retry is not supported with non-replayable request bodies")]
180 NonReplayableBody,
181
182 #[cfg(feature = "validate")]
184 #[error("request validation failed: {message}")]
185 RequestValidation {
186 message: String,
188 },
189
190 #[error("I/O error: {0}")]
192 Io(String),
193
194 #[error("configuration error: {0}")]
196 Config(String),
197
198 #[error("{0}")]
200 Other(String),
201}
202
203impl Error {
204 pub fn transport(kind: TransportKind, message: impl Into<String>) -> Self {
206 Self::Transport {
207 kind,
208 message: message.into(),
209 source: None,
210 }
211 }
212
213 pub fn transport_with_source(
215 kind: TransportKind,
216 message: impl Into<String>,
217 source: impl std::error::Error + Send + Sync + 'static,
218 ) -> Self {
219 Self::Transport {
220 kind,
221 message: message.into(),
222 source: Some(Arc::new(source)),
223 }
224 }
225
226 pub fn transport_message(message: impl Into<String>) -> Self {
228 Self::transport(TransportKind::Other, message)
229 }
230
231 pub fn transport_source(&self) -> Option<&(dyn std::error::Error + Send + Sync)> {
233 match self {
234 Self::Transport {
235 source: Some(s), ..
236 } => Some(s.as_ref()),
237 _ => None,
238 }
239 }
240
241 pub fn transport_kind(&self) -> Option<TransportKind> {
243 match self {
244 Self::Transport { kind, .. } => Some(*kind),
245 _ => None,
246 }
247 }
248
249 pub fn transport_detail(&self) -> Option<&str> {
251 match self {
252 Self::Transport { message, .. } => Some(message),
253 _ => None,
254 }
255 }
256
257 pub fn is_transport(&self) -> bool {
259 matches!(self, Self::Transport { .. })
260 }
261
262 pub fn is_timeout(&self) -> bool {
264 matches!(self, Self::Timeout)
265 }
266
267 pub fn http(status: StatusCode, message: impl Into<String>, body: Option<Bytes>) -> Self {
269 Self::http_with_status_text(
270 status,
271 status.canonical_reason().unwrap_or("").to_string(),
272 message,
273 body,
274 )
275 }
276
277 pub fn http_with_status_text(
279 status: StatusCode,
280 status_text: impl Into<String>,
281 message: impl Into<String>,
282 body: Option<Bytes>,
283 ) -> Self {
284 Self::Http {
285 status,
286 status_text: status_text.into(),
287 message: message.into(),
288 body,
289 }
290 }
291
292 pub(crate) fn http_error_for_status(status: StatusCode, body: Option<Bytes>) -> Self {
294 let status_text = status
295 .canonical_reason()
296 .unwrap_or("request failed")
297 .to_string();
298 let message = body
299 .as_ref()
300 .and_then(|b| std::str::from_utf8(b).ok())
301 .map(|s| s.chars().take(512).collect::<String>())
302 .filter(|s| !s.is_empty())
303 .unwrap_or_else(|| status_text.clone());
304 Self::http_with_status_text(status, status_text, message, body)
305 }
306
307 pub fn query_serialize(message: impl Into<String>) -> Self {
309 Self::QuerySerialize(message.into())
310 }
311
312 pub fn status(&self) -> Option<StatusCode> {
314 match self {
315 Self::Http { status, .. } => Some(*status),
316 #[cfg(feature = "json")]
317 Self::Deserialize { status, .. } => Some(*status),
318 #[cfg(feature = "validate")]
319 Self::Validation { status, .. } => Some(*status),
320 _ => None,
321 }
322 }
323
324 pub fn status_text(&self) -> Option<&str> {
326 match self {
327 Self::Http { status_text, .. } => Some(status_text),
328 _ => None,
329 }
330 }
331
332 pub fn body(&self) -> Option<&Bytes> {
334 match self {
335 Self::Http { body, .. } => body.as_ref(),
336 #[cfg(feature = "json")]
337 Self::Deserialize { body, .. } => body.as_ref(),
338 #[cfg(feature = "validate")]
339 Self::Validation { body, .. } => body.as_ref(),
340 _ => None,
341 }
342 }
343
344 pub fn is_retry_exhausted(&self) -> bool {
346 matches!(self, Self::RetryExhausted { .. })
347 }
348
349 pub fn retry_exhausted_last(&self) -> Option<&Error> {
351 match self {
352 Self::RetryExhausted { last, .. } => last.as_deref(),
353 _ => None,
354 }
355 }
356
357 pub fn is_cancelled(&self) -> bool {
359 matches!(self, Self::Cancelled)
360 }
361
362 pub fn is_body_too_large(&self) -> bool {
364 matches!(self, Self::BodyTooLarge { .. })
365 }
366
367 pub fn body_too_large_limit(&self) -> Option<u64> {
369 match self {
370 Self::BodyTooLarge { limit } => Some(*limit),
371 _ => None,
372 }
373 }
374
375 pub fn hook(msg: impl Into<String>) -> Self {
378 Self::Hook(msg.into())
379 }
380
381 pub fn is_hook(&self) -> bool {
383 matches!(self, Self::Hook(_))
384 }
385
386 pub(crate) fn retry_exhausted(attempts: u32, last: Error) -> Self {
387 Self::RetryExhausted {
388 attempts,
389 last: Some(Box::new(last)),
390 }
391 }
392
393 #[cfg(feature = "json")]
417 pub fn api_json<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
418 let body = self.body()?;
419 serde_json::from_slice(body).ok()
420 }
421
422 #[cfg(feature = "validate")]
424 pub fn api_json_validated<T>(&self) -> Option<T>
425 where
426 T: serde::de::DeserializeOwned + garde::Validate,
427 T::Context: Default,
428 {
429 let body = self.body()?;
430 let value: T = serde_json::from_slice(body).ok()?;
431 value.validate().ok()?;
432 Some(value)
433 }
434}
435
436pub(crate) fn map_transport_error(err: reqwest::Error) -> Error {
437 if err.is_timeout() {
438 return Error::Timeout;
439 }
440
441 let kind = transport_kind_from_reqwest(&err);
442 let message = err.to_string();
443 Error::Transport {
444 kind,
445 message,
446 source: Some(Arc::new(err)),
447 }
448}
449
450fn transport_kind_from_reqwest(err: &reqwest::Error) -> TransportKind {
451 #[cfg(not(target_arch = "wasm32"))]
452 if err.is_connect() {
453 return TransportKind::Connect;
454 }
455
456 if err.is_body() {
457 TransportKind::Body
458 } else if err.is_decode() {
459 TransportKind::Decode
460 } else if err.is_redirect() {
461 TransportKind::Redirect
462 } else if err.is_request() {
463 TransportKind::Request
464 } else if err.is_builder() {
465 TransportKind::Builder
466 } else if err.is_upgrade() {
467 TransportKind::Upgrade
468 } else {
469 TransportKind::Other
470 }
471}
472
473#[cfg(all(test, feature = "json"))]
474mod tests {
475 use super::*;
476 use serde::Deserialize;
477
478 #[derive(Debug, Deserialize, PartialEq)]
479 struct ApiError {
480 message: String,
481 }
482
483 #[test]
484 fn api_json_parses_http_body() {
485 let err = Error::http_with_status_text(
486 StatusCode::BAD_REQUEST,
487 "Bad Request",
488 "bad request",
489 Some(bytes::Bytes::from_static(br#"{"message":"invalid"}"#)),
490 );
491 let api: ApiError = err.api_json().unwrap();
492 assert_eq!(api.message, "invalid");
493 }
494
495 #[test]
496 fn status_and_status_text_accessors() {
497 let err = Error::http(StatusCode::NOT_FOUND, "not found", None);
498 assert_eq!(err.status(), Some(StatusCode::NOT_FOUND));
499 assert_eq!(err.status_text(), Some("Not Found"));
500 }
501
502 #[test]
503 fn api_json_returns_none_without_body() {
504 let err = Error::http(StatusCode::INTERNAL_SERVER_ERROR, "err", None);
505 assert!(err.api_json::<ApiError>().is_none());
506 }
507
508 #[test]
509 fn hook_constructor_and_is_hook() {
510 let err = Error::hook("blocked");
511 assert!(err.is_hook());
512 assert!(matches!(err, Error::Hook(msg) if msg == "blocked"));
513 }
514
515 #[test]
516 fn retry_exhausted_helper_sets_flag() {
517 let err = Error::retry_exhausted(3, Error::Timeout);
518 assert!(err.is_retry_exhausted());
519 assert!(matches!(
520 err,
521 Error::RetryExhausted {
522 attempts: 3,
523 last: Some(_)
524 }
525 ));
526 assert!(matches!(err.retry_exhausted_last(), Some(Error::Timeout)));
527 }
528
529 #[test]
530 fn transport_helpers() {
531 let err = Error::transport(TransportKind::Connect, "connection refused");
532 assert!(err.is_transport());
533 assert_eq!(err.transport_kind(), Some(TransportKind::Connect));
534 assert_eq!(err.transport_detail(), Some("connection refused"));
535 }
536
537 #[test]
538 fn transport_message_defaults_to_other() {
539 let err = Error::transport_message("tower layer failed");
540 assert_eq!(err.transport_kind(), Some(TransportKind::Other));
541 }
542}