1use crate::responses;
16use thiserror::Error;
17
18use backtrace::Backtrace;
19use reqwest::{
20 StatusCode, Url,
21 header::{HeaderMap, InvalidHeaderValue},
22};
23use serde::Deserialize;
24
25#[derive(Error, Debug)]
26pub enum ConversionError {
27 #[error("Unsupported argument value for property (field) {property}")]
28 UnsupportedPropertyValue { property: String },
29 #[error("Missing the required argument")]
30 MissingProperty { argument: String },
31 #[error("Could not parse a value: {message}")]
32 ParsingError { message: String },
33 #[error("Invalid type: expected {expected}")]
34 InvalidType { expected: String },
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
39pub struct ErrorDetails {
40 pub error: Option<String>,
42 pub reason: Option<String>,
44}
45
46impl ErrorDetails {
47 pub fn from_json(body: &str) -> Option<Self> {
48 serde_json::from_str(body).ok()
49 }
50
51 pub fn reason(&self) -> Option<&str> {
53 self.reason.as_deref().or(self.error.as_deref())
54 }
55}
56
57#[derive(Error, Debug)]
58pub enum Error<U, S, E, BT> {
59 #[error("API responded with a client error: status code of {status_code}")]
60 ClientErrorResponse {
61 url: Option<U>,
62 status_code: S,
63 body: Option<String>,
64 error_details: Option<ErrorDetails>,
65 headers: Option<HeaderMap>,
66 backtrace: BT,
67 },
68 #[error("API responded with a server error: status code of {status_code}")]
69 ServerErrorResponse {
70 url: Option<U>,
71 status_code: S,
72 body: Option<String>,
73 error_details: Option<ErrorDetails>,
74 headers: Option<HeaderMap>,
75 backtrace: BT,
76 },
77 #[error("Health check failed")]
78 HealthCheckFailed {
79 path: String,
80 details: responses::HealthCheckFailureDetails,
81 status_code: S,
82 },
83 #[error("API responded with a 404 Not Found")]
84 NotFound,
85 #[error(
86 "Cannot delete a binding: multiple matching bindings were found, provide additional properties"
87 )]
88 MultipleMatchingBindings,
89 #[error("could not convert provided value into an HTTP header value")]
90 InvalidHeaderValue { error: InvalidHeaderValue },
91 #[error("Unsupported argument value for property (field) {property}")]
92 UnsupportedArgumentValue { property: String },
93 #[error("Missing required argument")]
94 MissingProperty { argument: String },
95 #[error("Response is incompatible with the target data type")]
96 IncompatibleBody {
97 error: ConversionError,
98 backtrace: BT,
99 },
100 #[error("Could not parse a value: {message}")]
101 ParsingError { message: String },
102 #[error("encountered an error when performing an HTTP request")]
103 RequestError { error: E, backtrace: BT },
104 #[error("an unspecified error")]
105 Other,
106}
107
108#[allow(unused)]
109pub type HttpClientError = Error<Url, StatusCode, reqwest::Error, Backtrace>;
110
111impl From<reqwest::Error> for HttpClientError {
112 fn from(req_err: reqwest::Error) -> Self {
113 match req_err.status() {
114 None => HttpClientError::RequestError {
115 error: req_err,
116 backtrace: Backtrace::new(),
117 },
118 Some(status_code) => {
119 if status_code.is_client_error() {
120 return HttpClientError::ClientErrorResponse {
121 url: req_err.url().cloned(),
122 status_code,
123 body: None,
124 error_details: None,
125 headers: None,
126 backtrace: Backtrace::new(),
127 };
128 };
129
130 if status_code.is_server_error() {
131 return HttpClientError::ServerErrorResponse {
132 url: req_err.url().cloned(),
133 status_code,
134 body: None,
135 error_details: None,
136 headers: None,
137 backtrace: Backtrace::new(),
138 };
139 };
140
141 HttpClientError::RequestError {
142 error: req_err,
143 backtrace: Backtrace::new(),
144 }
145 }
146 }
147 }
148}
149
150impl From<InvalidHeaderValue> for HttpClientError {
151 fn from(err: InvalidHeaderValue) -> Self {
152 HttpClientError::InvalidHeaderValue { error: err }
153 }
154}
155
156impl From<ConversionError> for HttpClientError {
157 fn from(value: ConversionError) -> Self {
158 match value {
159 ConversionError::UnsupportedPropertyValue { property } => {
160 HttpClientError::UnsupportedArgumentValue { property }
161 }
162 ConversionError::MissingProperty { argument } => {
163 HttpClientError::MissingProperty { argument }
164 }
165 ConversionError::ParsingError { message } => HttpClientError::ParsingError { message },
166 ConversionError::InvalidType { expected } => HttpClientError::ParsingError {
167 message: format!("invalid type: expected {expected}"),
168 },
169 }
170 }
171}
172
173impl HttpClientError {
174 pub fn is_not_found(&self) -> bool {
176 matches!(self, HttpClientError::NotFound)
177 || matches!(
178 self,
179 HttpClientError::ClientErrorResponse { status_code, .. }
180 if *status_code == StatusCode::NOT_FOUND
181 )
182 }
183
184 pub fn is_already_exists(&self) -> bool {
186 matches!(
187 self,
188 HttpClientError::ClientErrorResponse { status_code, .. }
189 if *status_code == StatusCode::CONFLICT
190 )
191 }
192
193 pub fn is_unauthorized(&self) -> bool {
195 matches!(
196 self,
197 HttpClientError::ClientErrorResponse { status_code, .. }
198 if *status_code == StatusCode::UNAUTHORIZED
199 )
200 }
201
202 pub fn is_forbidden(&self) -> bool {
204 matches!(
205 self,
206 HttpClientError::ClientErrorResponse { status_code, .. }
207 if *status_code == StatusCode::FORBIDDEN
208 )
209 }
210
211 pub fn is_client_error(&self) -> bool {
213 matches!(
214 self,
215 HttpClientError::ClientErrorResponse { .. } | HttpClientError::NotFound
216 )
217 }
218
219 pub fn is_server_error(&self) -> bool {
221 matches!(self, HttpClientError::ServerErrorResponse { .. })
222 }
223
224 pub fn status_code(&self) -> Option<StatusCode> {
226 match self {
227 HttpClientError::ClientErrorResponse { status_code, .. } => Some(*status_code),
228 HttpClientError::ServerErrorResponse { status_code, .. } => Some(*status_code),
229 HttpClientError::HealthCheckFailed { status_code, .. } => Some(*status_code),
230 HttpClientError::NotFound => Some(StatusCode::NOT_FOUND),
231 _ => None,
232 }
233 }
234
235 pub fn url(&self) -> Option<&Url> {
237 match self {
238 HttpClientError::ClientErrorResponse { url, .. } => url.as_ref(),
239 HttpClientError::ServerErrorResponse { url, .. } => url.as_ref(),
240 HttpClientError::RequestError { error, .. } => error.url(),
241 _ => None,
242 }
243 }
244
245 pub fn error_details(&self) -> Option<&ErrorDetails> {
247 match self {
248 HttpClientError::ClientErrorResponse { error_details, .. } => error_details.as_ref(),
249 HttpClientError::ServerErrorResponse { error_details, .. } => error_details.as_ref(),
250 _ => None,
251 }
252 }
253
254 pub fn user_message(&self) -> String {
256 match self {
257 HttpClientError::ClientErrorResponse {
258 error_details,
259 status_code,
260 ..
261 } => {
262 if let Some(details) = error_details
263 && let Some(reason) = details.reason()
264 {
265 return reason.to_owned();
266 }
267 format!("Client error: {status_code}")
268 }
269 HttpClientError::ServerErrorResponse {
270 error_details,
271 status_code,
272 ..
273 } => {
274 if let Some(details) = error_details
275 && let Some(reason) = details.reason()
276 {
277 return reason.to_owned();
278 }
279 format!("Server error: {status_code}")
280 }
281 HttpClientError::HealthCheckFailed { details, .. } => {
282 format!("Health check failed: {}", details.reason())
283 }
284 HttpClientError::NotFound => "Resource not found".to_owned(),
285 HttpClientError::MultipleMatchingBindings => {
286 "Multiple matching bindings found, provide additional properties".to_owned()
287 }
288 HttpClientError::InvalidHeaderValue { .. } => "Invalid header value".to_owned(),
289 HttpClientError::UnsupportedArgumentValue { property } => {
290 format!("Unsupported value for property: {property}")
291 }
292 HttpClientError::MissingProperty { argument } => {
293 format!("Missing required argument: {argument}")
294 }
295 HttpClientError::IncompatibleBody { error, .. } => {
296 format!("Response parsing error: {error}")
297 }
298 HttpClientError::ParsingError { message } => format!("Parsing error: {message}"),
299 HttpClientError::RequestError { error, .. } => {
300 format!("Request error: {error}")
301 }
302 HttpClientError::Other => "An unspecified error occurred".to_owned(),
303 }
304 }
305}