rabbitmq_http_client/
error.rs

1// Copyright (C) 2023-2025 RabbitMQ Core Team (teamrabbitmq@gmail.com)
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// The API returns JSON with "error" and "reason" fields in error responses.
38#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
39pub struct ErrorDetails {
40    /// Generic error type, e.g., "bad_request"
41    pub error: Option<String>,
42    /// Detailed reason for the error
43    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    /// `reason` (typically more detailed) over `error` (generic).
52    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    /// Returns true if the error is a 404 Not Found response.
175    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    /// Returns true if the error indicates a resource already exists (409 Conflict).
185    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    /// Returns true if the error is a 401 Unauthorized response (not authenticated).
194    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    /// Returns true if the error is a 403 Forbidden response (authenticated but not authorized).
203    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    /// Returns true if the error is a client error (4xx status code).
212    pub fn is_client_error(&self) -> bool {
213        matches!(
214            self,
215            HttpClientError::ClientErrorResponse { .. } | HttpClientError::NotFound
216        )
217    }
218
219    /// Returns true if the error is a server error (5xx status code).
220    pub fn is_server_error(&self) -> bool {
221        matches!(self, HttpClientError::ServerErrorResponse { .. })
222    }
223
224    /// Returns the HTTP status code, if available.
225    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    /// Returns the URL that caused the error, if available.
236    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    /// Returns the error details from the API response, if available.
246    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    /// Returns a user-friendly error message, preferring API-provided details when available.
255    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}