1use std::time::Duration;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
8pub enum Error {
9 #[error("Bad request: {message}")]
11 BadRequest {
12 message: String,
13 code: Option<String>,
14 details: Option<serde_json::Value>,
15 },
16
17 #[error("Unauthorized: {message}")]
19 Unauthorized { message: String },
20
21 #[error("Forbidden: {message}")]
23 Forbidden { message: String },
24
25 #[error("Not found: {message}")]
27 NotFound { message: String },
28
29 #[error("Conflict: {message}")]
31 Conflict { message: String },
32
33 #[error("Validation error: {message}")]
35 Validation {
36 message: String,
37 details: Option<serde_json::Value>,
38 },
39
40 #[error("Rate limited (retry after {retry_after:?})")]
42 RateLimited {
43 message: String,
44 retry_after: Option<Duration>,
45 },
46
47 #[error("Server error ({status}): {message}")]
49 Server {
50 status: u16,
51 message: String,
52 code: Option<String>,
53 },
54
55 #[error("Network error: {0}")]
57 Network(#[from] reqwest::Error),
58
59 #[error("JSON error: {0}")]
61 Json(#[from] serde_json::Error),
62
63 #[error("URL error: {0}")]
65 Url(#[from] url::ParseError),
66
67 #[error("Configuration error: {0}")]
69 Config(String),
70
71 #[error("Timeout after {duration:?} waiting for {operation}")]
73 Timeout {
74 operation: String,
75 duration: Duration,
76 },
77}
78
79pub type Result<T> = std::result::Result<T, Error>;
81
82#[derive(Debug, Clone, serde::Deserialize)]
84pub(crate) struct ErrorResponse {
85 #[serde(default)]
86 pub code: String,
87 #[serde(default)]
88 pub message: String,
89 #[serde(default)]
90 pub details: Option<serde_json::Value>,
91}
92
93impl Error {
94 pub(crate) async fn from_response(resp: reqwest::Response) -> Self {
96 let status = resp.status().as_u16();
97 let retry_after = resp
98 .headers()
99 .get("retry-after")
100 .and_then(|v| v.to_str().ok())
101 .and_then(|v| v.parse::<u64>().ok())
102 .map(Duration::from_secs);
103
104 let body = resp.json::<ErrorResponse>().await.ok();
105 let message = body
106 .as_ref()
107 .map(|b| b.message.clone())
108 .unwrap_or_else(|| format!("HTTP {status}"));
109 let code = body.as_ref().map(|b| b.code.clone());
110 let details = body.as_ref().and_then(|b| b.details.clone());
111
112 match status {
113 400 => Error::BadRequest {
114 message,
115 code,
116 details,
117 },
118 401 => Error::Unauthorized { message },
119 403 => Error::Forbidden { message },
120 404 => Error::NotFound { message },
121 409 => Error::Conflict { message },
122 422 => Error::Validation {
123 message,
124 details,
125 },
126 429 => Error::RateLimited {
127 message,
128 retry_after,
129 },
130 _ => Error::Server {
131 status,
132 message,
133 code,
134 },
135 }
136 }
137
138 pub fn is_retryable(&self) -> bool {
140 matches!(
141 self,
142 Error::RateLimited { .. }
143 | Error::Server {
144 status: 500 | 502 | 503 | 504,
145 ..
146 }
147 | Error::Network(_)
148 )
149 }
150
151 pub fn status_code(&self) -> Option<u16> {
153 match self {
154 Error::BadRequest { .. } => Some(400),
155 Error::Unauthorized { .. } => Some(401),
156 Error::Forbidden { .. } => Some(403),
157 Error::NotFound { .. } => Some(404),
158 Error::Conflict { .. } => Some(409),
159 Error::Validation { .. } => Some(422),
160 Error::RateLimited { .. } => Some(429),
161 Error::Server { status, .. } => Some(*status),
162 _ => None,
163 }
164 }
165}