1use std::time::{Duration, SystemTime};
4
5use chrono::{DateTime, Utc};
6use httpdate::parse_http_date;
7use reqwest::StatusCode;
8use reqwest::header::{HeaderMap, HeaderValue};
9
10#[derive(Debug, thiserror::Error)]
12#[non_exhaustive]
13pub enum HonchoError {
14 #[error("Honcho API error: HTTP 400 {message}")]
16 BadRequest {
17 message: String,
19 body: Option<serde_json::Value>,
21 },
22 #[error("Honcho API error: HTTP 401 {message}")]
24 Authentication {
25 message: String,
27 },
28 #[error("Honcho API error: HTTP 403 {message}")]
30 PermissionDenied {
31 message: String,
33 },
34 #[error("Honcho API error: HTTP 404 {message}")]
36 NotFound {
37 message: String,
39 },
40 #[error("Honcho API error: HTTP 409 {message}")]
42 Conflict {
43 message: String,
45 body: Option<serde_json::Value>,
47 },
48 #[error("Honcho API error: HTTP 422 {message}")]
50 UnprocessableEntity {
51 message: String,
53 body: Option<serde_json::Value>,
55 },
56 #[error("Honcho API error: HTTP 429 {message}")]
58 RateLimit {
59 message: String,
61 retry_after: Option<Duration>,
63 },
64 #[error("Honcho API error: HTTP {status} {message}")]
71 Client {
72 status: u16,
74 message: String,
76 },
77 #[error("Honcho API error: HTTP {status} {message}")]
79 Server {
80 status: u16,
82 message: String,
84 },
85 #[error("Request timed out: {message}")]
87 Timeout {
88 message: String,
90 },
91 #[error("Connection error: {message}")]
93 Connection {
94 message: String,
96 },
97 #[error(transparent)]
99 Transport(#[from] reqwest::Error),
100 #[error("Failed to decode response at {path}: {source}")]
102 Decode {
103 path: String,
105 #[source]
107 source: serde_json::Error,
108 },
109 #[error("Failed to serialize {path}: {source}")]
111 Serialization {
112 path: String,
114 #[source]
116 source: serde_json::Error,
117 },
118 #[error(transparent)]
120 Io(#[from] std::io::Error),
121 #[error("Configuration error: {0}")]
123 Configuration(String),
124 #[error("Validation error: {0}")]
126 Validation(String),
127 #[error("Partial failure after {sent} messages: {error}")]
133 PartialFailure {
134 messages: Vec<crate::Message>,
136 sent: usize,
138 #[source]
140 error: Box<HonchoError>,
141 },
142}
143
144impl HonchoError {
145 #[must_use]
149 pub fn code(&self) -> &'static str {
150 match self {
151 Self::BadRequest { .. } => "bad_request",
152 Self::Authentication { .. } => "authentication_error",
153 Self::PermissionDenied { .. } => "permission_denied",
154 Self::NotFound { .. } => "not_found",
155 Self::Conflict { .. } => "conflict",
156 Self::UnprocessableEntity { .. } => "unprocessable_entity",
157 Self::RateLimit { .. } => "rate_limit_exceeded",
158 Self::Client { .. } => "client_error",
159 Self::Server { .. } => "server_error",
160 Self::Timeout { .. } => "timeout",
161 Self::Connection { .. } => "connection_error",
162 Self::Transport(_) => "transport_error",
163 Self::Decode { .. } => "decode_error",
164 Self::Serialization { .. } => "serialization_error",
165 Self::Io(_) => "io_error",
166 Self::Configuration(_) => "configuration_error",
167 Self::Validation(_) => "validation_error",
168 Self::PartialFailure { .. } => "partial_failure",
169 }
170 }
171
172 #[must_use]
174 pub fn status_code(&self) -> Option<u16> {
175 match self {
176 Self::BadRequest { .. } => Some(400),
177 Self::Authentication { .. } => Some(401),
178 Self::PermissionDenied { .. } => Some(403),
179 Self::NotFound { .. } => Some(404),
180 Self::Conflict { .. } => Some(409),
181 Self::UnprocessableEntity { .. } => Some(422),
182 Self::RateLimit { .. } => Some(429),
183 Self::Client { status, .. } | Self::Server { status, .. } => Some(*status),
184 Self::Timeout { .. }
185 | Self::Connection { .. }
186 | Self::Transport(_)
187 | Self::Decode { .. }
188 | Self::Serialization { .. }
189 | Self::Io(_)
190 | Self::Configuration(_)
191 | Self::Validation(_) => None,
192 Self::PartialFailure { error, .. } => error.status_code(),
193 }
194 }
195
196 #[must_use]
205 pub fn is_retryable(&self) -> bool {
206 if matches!(self, Self::PartialFailure { .. }) {
207 return false;
208 }
209 matches!(self, Self::Timeout { .. } | Self::Connection { .. })
210 || matches!(self.status_code(), Some(429 | 500 | 502 | 503 | 504))
211 }
212
213 #[must_use]
219 pub fn retry_after(&self) -> Option<Duration> {
220 match self {
221 Self::RateLimit { retry_after, .. } => *retry_after,
222 Self::PartialFailure { error, .. } => error.retry_after(),
223 _ => None,
224 }
225 }
226
227 #[must_use]
229 pub fn is_partial_failure(&self) -> bool {
230 matches!(self, Self::PartialFailure { .. })
231 }
232
233 #[must_use]
238 pub fn into_partial_failure(self) -> Option<(Vec<crate::Message>, Box<HonchoError>)> {
239 match self {
240 Self::PartialFailure {
241 messages, error, ..
242 } => Some((messages, error)),
243 _ => None,
244 }
245 }
246
247 #[must_use]
255 #[allow(clippy::match_same_arms)]
259 pub fn message(&self) -> &str {
260 match self {
261 Self::BadRequest { message, .. } => message,
262 Self::Authentication { message } => message,
263 Self::PermissionDenied { message } => message,
264 Self::NotFound { message } => message,
265 Self::Conflict { message, .. } => message,
266 Self::UnprocessableEntity { message, .. } => message,
267 Self::RateLimit { message, .. } => message,
268 Self::Client { message, .. } => message,
269 Self::Server { message, .. } => message,
270 Self::Timeout { message } => message,
271 Self::Connection { message } => message,
272 Self::Transport(_) => "transport error",
273 Self::Io(_) => "I/O error",
274 Self::Decode { .. } => "failed to decode response",
275 Self::Serialization { .. } => "failed to serialize request",
276 Self::Configuration(s) => s,
277 Self::Validation(s) => s,
278 Self::PartialFailure { error, .. } => error.message(),
279 }
280 }
281}
282
283pub type Result<T> = std::result::Result<T, HonchoError>;
285
286#[must_use]
290pub fn parse_error_body(body: &[u8]) -> (String, Option<serde_json::Value>) {
291 let Ok(value) = serde_json::from_slice::<serde_json::Value>(body) else {
292 let msg = String::from_utf8_lossy(body).into_owned();
293 return (msg, None);
294 };
295
296 if let Some(obj) = value.as_object() {
297 if let Some(readable) = obj.get("detail").and_then(detail_message) {
298 return (readable, Some(value));
299 }
300 if let Some(message) = obj.get("message").and_then(|v| v.as_str()) {
301 return (message.to_string(), Some(value));
302 }
303 if let Some(error) = obj.get("error").and_then(|v| v.as_str()) {
304 return (error.to_string(), Some(value));
305 }
306 return (value.to_string(), Some(value));
307 }
308
309 if let Some(s) = value.as_str() {
310 return (s.to_string(), Some(value));
311 }
312
313 (value.to_string(), Some(value))
314}
315
316fn detail_message(detail: &serde_json::Value) -> Option<String> {
326 match detail {
327 serde_json::Value::String(s) => Some(s.clone()),
328 serde_json::Value::Array(arr) => {
329 let parts: Vec<String> = arr.iter().filter_map(item_message).collect();
330 if parts.is_empty() {
331 None
332 } else {
333 Some(parts.join("; "))
334 }
335 }
336 _ => None,
337 }
338}
339
340fn item_message(item: &serde_json::Value) -> Option<String> {
342 match item {
343 serde_json::Value::String(s) => Some(s.clone()),
344 serde_json::Value::Object(obj) => {
345 obj.get("msg").and_then(|m| m.as_str()).map(str::to_owned)
346 }
347 _ => None,
348 }
349}
350
351#[must_use]
367pub fn parse_retry_after(value: &HeaderValue, now: DateTime<Utc>) -> Option<Duration> {
368 let s = value.to_str().ok()?;
369
370 if let Ok(secs) = s.parse::<f64>() {
371 if !secs.is_finite() {
375 return None;
376 }
377 return Duration::try_from_secs_f64(secs.max(0.0)).ok();
378 }
379
380 let target = parse_http_date(s).ok()?;
381 let now_systime: SystemTime = now.into();
382 match target.duration_since(now_systime) {
383 Ok(diff) => Some(diff),
384 Err(_) => Some(Duration::ZERO),
385 }
386}
387
388#[must_use]
390pub fn from_response(
391 status: StatusCode,
392 headers: &HeaderMap,
393 body: &bytes::Bytes,
394 now: DateTime<Utc>,
395) -> HonchoError {
396 let (message, body_value) = parse_error_body(body);
397
398 match status.as_u16() {
399 400 => HonchoError::BadRequest {
400 message,
401 body: body_value,
402 },
403 401 => HonchoError::Authentication { message },
404 403 => HonchoError::PermissionDenied { message },
405 404 => HonchoError::NotFound { message },
406 409 => HonchoError::Conflict {
407 message,
408 body: body_value,
409 },
410 422 => HonchoError::UnprocessableEntity {
411 message,
412 body: body_value,
413 },
414 429 => {
415 let retry_after = headers
416 .get(reqwest::header::RETRY_AFTER)
417 .and_then(|v| parse_retry_after(v, now));
418 HonchoError::RateLimit {
419 message,
420 retry_after,
421 }
422 }
423 s if s >= 500 => HonchoError::Server { status: s, message },
424 s if (400..500).contains(&s) => HonchoError::Client { status: s, message },
425 s if (300..400).contains(&s) => HonchoError::Client {
426 status: s,
427 message: format!("unexpected redirect status {s}"),
428 },
429 _ => HonchoError::Client {
430 status: status.as_u16(),
431 message: format!("unexpected response status {}", status.as_u16()),
432 },
433 }
434}