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")]
113 BodyTooLarge {
114 limit: u64,
116 },
117
118 #[error("client base URL is required; call ClientBuilder::base_url")]
120 MissingBaseUrl,
121
122 #[error("retries exhausted after {attempts} attempts")]
124 RetryExhausted {
125 attempts: u32,
127 last: Option<Box<Error>>,
129 },
130
131 #[error("hook error: {0}")]
135 Hook(String),
136
137 #[error("failed to serialize query: {0}")]
142 QuerySerialize(String),
143
144 #[error("invalid header name: {0}")]
146 InvalidHeaderName(String),
147
148 #[error("invalid header value: {0}")]
150 InvalidHeaderValue(String),
151
152 #[error("missing path parameter for `{0}`")]
154 MissingPathParam(String),
155
156 #[error("route not in schema registry: {method} {path}")]
158 SchemaRoute {
159 method: String,
161 path: String,
163 },
164
165 #[cfg(feature = "schema-validate")]
167 #[error("JSON schema validation failed ({phase}): {message}")]
168 SchemaValidation {
169 phase: &'static str,
171 message: String,
173 },
174
175 #[error("invalid authorization header: {0}")]
177 InvalidAuthHeader(String),
178
179 #[error("automatic retry is not supported with non-replayable request bodies")]
181 NonReplayableBody,
182
183 #[cfg(feature = "validate")]
185 #[error("request validation failed: {message}")]
186 RequestValidation {
187 message: String,
189 },
190
191 #[error("I/O error: {0}")]
193 Io(String),
194
195 #[error("configuration error: {0}")]
197 Config(String),
198
199 #[error("{0}")]
201 Other(String),
202}
203
204impl Error {
205 pub fn transport(kind: TransportKind, message: impl Into<String>) -> Self {
207 Self::Transport {
208 kind,
209 message: message.into(),
210 source: None,
211 }
212 }
213
214 pub fn transport_with_source(
216 kind: TransportKind,
217 message: impl Into<String>,
218 source: impl std::error::Error + Send + Sync + 'static,
219 ) -> Self {
220 Self::Transport {
221 kind,
222 message: message.into(),
223 source: Some(Arc::new(source)),
224 }
225 }
226
227 pub fn transport_message(message: impl Into<String>) -> Self {
229 Self::transport(TransportKind::Other, message)
230 }
231
232 pub fn transport_source(&self) -> Option<&(dyn std::error::Error + Send + Sync)> {
234 match self {
235 Self::Transport {
236 source: Some(s), ..
237 } => Some(s.as_ref()),
238 _ => None,
239 }
240 }
241
242 pub fn transport_kind(&self) -> Option<TransportKind> {
244 match self {
245 Self::Transport { kind, .. } => Some(*kind),
246 _ => None,
247 }
248 }
249
250 pub fn transport_detail(&self) -> Option<&str> {
252 match self {
253 Self::Transport { message, .. } => Some(message),
254 _ => None,
255 }
256 }
257
258 pub fn is_transport(&self) -> bool {
260 matches!(self, Self::Transport { .. })
261 }
262
263 pub fn is_timeout(&self) -> bool {
265 matches!(self, Self::Timeout)
266 }
267
268 pub fn http(status: StatusCode, message: impl Into<String>, body: Option<Bytes>) -> Self {
270 Self::http_with_status_text(
271 status,
272 status.canonical_reason().unwrap_or("").to_string(),
273 message,
274 body,
275 )
276 }
277
278 pub fn http_with_status_text(
280 status: StatusCode,
281 status_text: impl Into<String>,
282 message: impl Into<String>,
283 body: Option<Bytes>,
284 ) -> Self {
285 Self::Http {
286 status,
287 status_text: status_text.into(),
288 message: message.into(),
289 body,
290 }
291 }
292
293 pub(crate) fn http_error_for_status(status: StatusCode, body: Option<Bytes>) -> Self {
295 let status_text = status
296 .canonical_reason()
297 .unwrap_or("request failed")
298 .to_string();
299 let message = body
300 .as_ref()
301 .and_then(|b| std::str::from_utf8(b).ok())
302 .map(|s| s.chars().take(512).collect::<String>())
303 .filter(|s| !s.is_empty())
304 .unwrap_or_else(|| status_text.clone());
305 Self::http_with_status_text(status, status_text, message, body)
306 }
307
308 pub fn query_serialize(message: impl Into<String>) -> Self {
310 Self::QuerySerialize(message.into())
311 }
312
313 pub fn status(&self) -> Option<StatusCode> {
315 match self {
316 Self::Http { status, .. } => Some(*status),
317 #[cfg(feature = "json")]
318 Self::Deserialize { status, .. } => Some(*status),
319 #[cfg(feature = "validate")]
320 Self::Validation { status, .. } => Some(*status),
321 _ => None,
322 }
323 }
324
325 pub fn status_text(&self) -> Option<&str> {
327 match self {
328 Self::Http { status_text, .. } => Some(status_text),
329 _ => None,
330 }
331 }
332
333 pub fn body(&self) -> Option<&Bytes> {
335 match self {
336 Self::Http { body, .. } => body.as_ref(),
337 #[cfg(feature = "json")]
338 Self::Deserialize { body, .. } => body.as_ref(),
339 #[cfg(feature = "validate")]
340 Self::Validation { body, .. } => body.as_ref(),
341 _ => None,
342 }
343 }
344
345 pub fn is_retry_exhausted(&self) -> bool {
347 matches!(self, Self::RetryExhausted { .. })
348 }
349
350 pub fn retry_exhausted_last(&self) -> Option<&Error> {
352 match self {
353 Self::RetryExhausted { last, .. } => last.as_deref(),
354 _ => None,
355 }
356 }
357
358 pub fn is_cancelled(&self) -> bool {
360 matches!(self, Self::Cancelled)
361 }
362
363 pub fn is_body_too_large(&self) -> bool {
365 matches!(self, Self::BodyTooLarge { .. })
366 }
367
368 pub fn body_too_large_limit(&self) -> Option<u64> {
370 match self {
371 Self::BodyTooLarge { limit } => Some(*limit),
372 _ => None,
373 }
374 }
375
376 pub fn hook(msg: impl Into<String>) -> Self {
379 Self::Hook(msg.into())
380 }
381
382 pub fn is_hook(&self) -> bool {
384 matches!(self, Self::Hook(_))
385 }
386
387 pub(crate) fn retry_exhausted(attempts: u32, last: Error) -> Self {
388 Self::RetryExhausted {
389 attempts,
390 last: Some(Box::new(last)),
391 }
392 }
393
394 #[cfg(feature = "json")]
418 pub fn api_json<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
419 let body = self.body()?;
420 serde_json::from_slice(body).ok()
421 }
422
423 #[cfg(feature = "validate")]
425 pub fn api_json_validated<T>(&self) -> Option<T>
426 where
427 T: serde::de::DeserializeOwned + garde::Validate,
428 T::Context: Default,
429 {
430 let body = self.body()?;
431 let value: T = serde_json::from_slice(body).ok()?;
432 value.validate().ok()?;
433 Some(value)
434 }
435}
436
437pub(crate) fn map_transport_error(err: reqwest::Error) -> Error {
438 if err.is_timeout() {
439 return Error::Timeout;
440 }
441
442 let kind = transport_kind_from_reqwest(&err);
443 let message = err.to_string();
444 Error::Transport {
445 kind,
446 message,
447 source: Some(Arc::new(err)),
448 }
449}
450
451fn transport_kind_from_reqwest(err: &reqwest::Error) -> TransportKind {
452 #[cfg(not(target_arch = "wasm32"))]
453 if err.is_connect() {
454 return TransportKind::Connect;
455 }
456
457 if err.is_body() {
458 TransportKind::Body
459 } else if err.is_decode() {
460 TransportKind::Decode
461 } else if err.is_redirect() {
462 TransportKind::Redirect
463 } else if err.is_request() {
464 TransportKind::Request
465 } else if err.is_builder() {
466 TransportKind::Builder
467 } else if err.is_upgrade() {
468 TransportKind::Upgrade
469 } else {
470 TransportKind::Other
471 }
472}
473
474#[cfg(all(test, feature = "json"))]
475mod tests {
476 use super::*;
477 use serde::Deserialize;
478
479 #[derive(Debug, Deserialize, PartialEq)]
480 struct ApiError {
481 message: String,
482 }
483
484 #[test]
485 fn api_json_parses_http_body() {
486 let err = Error::http_with_status_text(
487 StatusCode::BAD_REQUEST,
488 "Bad Request",
489 "bad request",
490 Some(bytes::Bytes::from_static(br#"{"message":"invalid"}"#)),
491 );
492 let api: ApiError = err.api_json().unwrap();
493 assert_eq!(api.message, "invalid");
494 }
495
496 #[test]
497 fn status_and_status_text_accessors() {
498 let err = Error::http(StatusCode::NOT_FOUND, "not found", None);
499 assert_eq!(err.status(), Some(StatusCode::NOT_FOUND));
500 assert_eq!(err.status_text(), Some("Not Found"));
501 }
502
503 #[test]
504 fn api_json_returns_none_without_body() {
505 let err = Error::http(StatusCode::INTERNAL_SERVER_ERROR, "err", None);
506 assert!(err.api_json::<ApiError>().is_none());
507 }
508
509 #[test]
510 fn hook_constructor_and_is_hook() {
511 let err = Error::hook("blocked");
512 assert!(err.is_hook());
513 assert!(matches!(err, Error::Hook(msg) if msg == "blocked"));
514 }
515
516 #[test]
517 fn retry_exhausted_helper_sets_flag() {
518 let err = Error::retry_exhausted(3, Error::Timeout);
519 assert!(err.is_retry_exhausted());
520 assert!(matches!(
521 err,
522 Error::RetryExhausted {
523 attempts: 3,
524 last: Some(_)
525 }
526 ));
527 assert!(matches!(err.retry_exhausted_last(), Some(Error::Timeout)));
528 }
529
530 #[test]
531 fn transport_helpers() {
532 let err = Error::transport(TransportKind::Connect, "connection refused");
533 assert!(err.is_transport());
534 assert_eq!(err.transport_kind(), Some(TransportKind::Connect));
535 assert_eq!(err.transport_detail(), Some("connection refused"));
536 }
537
538 #[test]
539 fn transport_message_defaults_to_other() {
540 let err = Error::transport_message("tower layer failed");
541 assert_eq!(err.transport_kind(), Some(TransportKind::Other));
542 }
543}