1use std::fmt;
10
11#[derive(Debug, Clone)]
14pub struct ErrorDetails {
15 pub message: String,
18 pub status: Option<u16>,
20 pub code: Option<String>,
22 pub request_id: Option<String>,
24 pub feature: Option<String>,
26 pub retry_after_seconds: Option<u64>,
28 pub body: Option<serde_json::Value>,
30}
31
32impl fmt::Display for ErrorDetails {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 write!(f, "{}", self.message)?;
35 if let Some(status) = self.status {
36 write!(f, " (status={status}")?;
37 if let Some(rid) = &self.request_id {
38 write!(f, " request_id={rid}")?;
39 }
40 write!(f, ")")?;
41 }
42 Ok(())
43 }
44}
45
46#[derive(Debug, thiserror::Error)]
48#[non_exhaustive]
49pub enum Error {
50 #[error("authentication failed: {0}")]
52 Auth(Box<ErrorDetails>),
53
54 #[error("plan does not allow this feature: {0}")]
57 Plan(Box<ErrorDetails>),
58
59 #[error("rate limit exceeded: {0}")]
61 RateLimit(Box<ErrorDetails>),
62
63 #[error("invalid request: {0}")]
65 Validation(Box<ErrorDetails>),
66
67 #[error("not found: {0}")]
69 NotFound(Box<ErrorDetails>),
70
71 #[error("conflict: {0}")]
73 Conflict(Box<ErrorDetails>),
74
75 #[error("gateway error: {0}")]
77 Server(Box<ErrorDetails>),
78
79 #[error("api error: {0}")]
81 Api(Box<ErrorDetails>),
82
83 #[error("request timed out: {0}")]
85 Timeout(String),
86
87 #[error("connection error: {0}")]
89 Connection(#[source] reqwest::Error),
90
91 #[error("failed to decode response: {0}")]
93 Decode(String),
94
95 #[error("invalid configuration: {0}")]
97 Config(String),
98}
99
100impl Error {
101 #[must_use]
103 pub fn details(&self) -> Option<&ErrorDetails> {
104 match self {
105 Error::Auth(d)
106 | Error::Plan(d)
107 | Error::RateLimit(d)
108 | Error::Validation(d)
109 | Error::NotFound(d)
110 | Error::Conflict(d)
111 | Error::Server(d)
112 | Error::Api(d) => Some(d),
113 Error::Timeout(_) | Error::Connection(_) | Error::Decode(_) | Error::Config(_) => None,
114 }
115 }
116
117 #[must_use]
119 pub fn status(&self) -> Option<u16> {
120 self.details().and_then(|d| d.status)
121 }
122
123 #[must_use]
125 pub fn request_id(&self) -> Option<&str> {
126 self.details().and_then(|d| d.request_id.as_deref())
127 }
128
129 #[must_use]
131 pub fn feature(&self) -> Option<&str> {
132 self.details().and_then(|d| d.feature.as_deref())
133 }
134
135 #[must_use]
137 pub fn retry_after_seconds(&self) -> Option<u64> {
138 self.details().and_then(|d| d.retry_after_seconds)
139 }
140}
141
142pub(crate) fn from_status(
146 status: u16,
147 body: Option<serde_json::Value>,
148 request_id: Option<String>,
149 retry_after_seconds: Option<u64>,
150) -> Error {
151 let err_obj = body.as_ref().and_then(|b| b.get("error"));
152 let message = err_obj
153 .and_then(|e| e.get("message"))
154 .and_then(|m| m.as_str())
155 .map(str::to_owned)
156 .unwrap_or_else(|| format!("HTTP {status}"));
157 let code = err_obj
158 .and_then(|e| e.get("code"))
159 .and_then(|c| c.as_str())
160 .map(str::to_owned);
161 let feature = err_obj
162 .and_then(|e| e.get("feature"))
163 .and_then(|f| f.as_str())
164 .map(str::to_owned);
165
166 let details = Box::new(ErrorDetails {
167 message,
168 status: Some(status),
169 code,
170 request_id,
171 feature: feature.clone(),
172 retry_after_seconds,
173 body,
174 });
175
176 match status {
177 400 => Error::Validation(details),
178 401 => Error::Auth(details),
179 403 if feature.is_some() => Error::Plan(details),
180 403 => Error::Auth(details),
181 404 => Error::NotFound(details),
182 409 => Error::Conflict(details),
183 429 => Error::RateLimit(details),
184 s if s >= 500 => Error::Server(details),
185 _ => Error::Api(details),
186 }
187}
188
189pub type Result<T> = std::result::Result<T, Error>;