1use std::collections::HashMap;
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8pub type Result<T, E = Error> = std::result::Result<T, E>;
9
10#[derive(Debug, thiserror::Error)]
12pub enum Error {
13 #[error(transparent)]
17 Api(Box<ApiError>),
18
19 #[error("connection error: {0}")]
21 Connection(#[source] reqwest::Error),
22
23 #[error("request timed out")]
25 Timeout,
26
27 #[error("failed to decode response body: {0}")]
29 Decode(#[source] serde_json::Error),
30
31 #[error("invalid client configuration: {0}")]
33 Config(String),
34
35 #[error("failed to encode request: {0}")]
37 Encode(String),
38
39 #[error("event stream error: {0}")]
41 Stream(String),
42}
43
44impl From<ApiError> for Error {
45 fn from(error: ApiError) -> Self {
46 Error::Api(Box::new(error))
47 }
48}
49
50impl Error {
51 pub fn status(&self) -> Option<u16> {
53 match self {
54 Error::Api(err) => Some(err.status),
55 _ => None,
56 }
57 }
58
59 pub fn is_retryable(&self) -> bool {
61 match self {
62 Error::Api(err) => err.is_retryable(),
63 Error::Connection(_) | Error::Timeout => true,
64 _ => false,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
71pub struct Problem {
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub r#type: Option<String>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub title: Option<String>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub status: Option<i64>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub detail: Option<String>,
80 #[serde(
82 rename = "correlationId",
83 default,
84 skip_serializing_if = "Option::is_none"
85 )]
86 pub correlation_id: Option<String>,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub errors: Vec<FieldError>,
90 #[serde(flatten)]
92 pub extra: HashMap<String, serde_json::Value>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct FieldError {
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub field: Option<String>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub message: Option<String>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105#[non_exhaustive]
106pub enum ApiErrorKind {
107 BadRequest,
108 Authentication,
109 PermissionDenied,
110 NotFound,
111 Conflict,
112 Gone,
113 PayloadTooLarge,
114 UnprocessableEntity,
115 RateLimit,
116 ServiceUnavailable,
117 Server,
118 Other,
119}
120
121#[derive(Debug, Clone)]
123pub struct ApiError {
124 pub status: u16,
125 pub problem: Problem,
126 pub headers: HashMap<String, String>,
128}
129
130impl ApiError {
131 pub fn kind(&self) -> ApiErrorKind {
132 match self.status {
133 400 => ApiErrorKind::BadRequest,
134 401 => ApiErrorKind::Authentication,
135 403 => ApiErrorKind::PermissionDenied,
136 404 => ApiErrorKind::NotFound,
137 409 => ApiErrorKind::Conflict,
138 410 => ApiErrorKind::Gone,
139 413 => ApiErrorKind::PayloadTooLarge,
140 422 => ApiErrorKind::UnprocessableEntity,
141 429 => ApiErrorKind::RateLimit,
142 503 => ApiErrorKind::ServiceUnavailable,
143 status if status >= 500 => ApiErrorKind::Server,
144 _ => ApiErrorKind::Other,
145 }
146 }
147
148 pub fn correlation_id(&self) -> Option<&str> {
150 self.problem
151 .correlation_id
152 .as_deref()
153 .or_else(|| self.headers.get("x-correlation-id").map(String::as_str))
154 }
155
156 pub fn retry_after_seconds(&self) -> Option<f64> {
158 self.headers.get("retry-after")?.parse().ok()
159 }
160
161 pub fn rate_limit_remaining(&self) -> Option<i64> {
163 self.headers.get("x-ratelimit-remaining")?.parse().ok()
164 }
165
166 pub fn is_retryable(&self) -> bool {
167 matches!(self.status, 408 | 409 | 429 | 500 | 502 | 503 | 504)
168 }
169}
170
171impl fmt::Display for ApiError {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 let title = self.problem.title.as_deref().unwrap_or("HTTP error");
174 write!(f, "{} {}", self.status, title)?;
175 if let Some(detail) = &self.problem.detail {
176 write!(f, " — {detail}")?;
177 }
178 if let Some(correlation) = self.correlation_id() {
179 write!(f, " (correlationId: {correlation})")?;
180 }
181 Ok(())
182 }
183}
184
185impl std::error::Error for ApiError {}