Skip to main content

timeweb_rs/
error_body.rs

1//! Uniform view of the API's error response bodies.
2
3use serde::Deserialize;
4
5use crate::apis::Error;
6
7/// The error envelope every Timeweb Cloud error response shares.
8///
9/// The generated operations type each error body per operation and status,
10/// which makes uniform handling (logging, user messages) awkward. All those
11/// bodies carry the same envelope, and this type extracts it from any
12/// operation error without naming the per-operation entity:
13///
14/// ```no_run
15/// # async fn run() {
16/// use timeweb_rs::{ErrorDetails, apis::servers_api};
17///
18/// let config = timeweb_rs::authenticated("your-jwt-token");
19/// if let Err(error) = servers_api::get_server(&config, 42).await {
20///     if let Some(details) = ErrorDetails::from_api_error(&error) {
21///         eprintln!("{}: {}", details.status_code, details.messages().join("; "));
22///     }
23/// }
24/// # }
25/// ```
26#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
27pub struct ErrorDetails {
28    /// HTTP status code repeated in the body.
29    pub status_code: u16,
30    /// Machine-readable error identifier.
31    pub error_code:  String,
32    /// One or several human-readable messages.
33    #[serde(default)]
34    pub message:     Option<ErrorMessage>,
35    /// Request correlation id for support tickets.
36    #[serde(default)]
37    pub response_id: Option<String>
38}
39
40/// An error body's `message` field: the API sends a string or a list.
41#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
42#[serde(untagged)]
43pub enum ErrorMessage {
44    /// A single message.
45    One(String),
46    /// Several messages, typically one per invalid field.
47    Many(Vec<String>)
48}
49
50impl ErrorDetails {
51    /// Extracts the error envelope from any operation error.
52    ///
53    /// Returns `None` for transport, serialization and IO errors, and for
54    /// response bodies that do not carry the envelope.
55    #[must_use]
56    pub fn from_api_error<E>(error: &Error<E>) -> Option<Self> {
57        match error {
58            Error::ResponseError(response) => serde_json::from_str(&response.content).ok(),
59            Error::Reqwest(_) | Error::Serde(_) | Error::Io(_) => None
60        }
61    }
62
63    /// Every message in the body, regardless of the field's shape.
64    #[must_use]
65    pub fn messages(&self) -> Vec<String> {
66        match &self.message {
67            None => Vec::new(),
68            Some(ErrorMessage::One(message)) => vec![message.clone()],
69            Some(ErrorMessage::Many(messages)) => messages.clone()
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::{ErrorDetails, ErrorMessage};
77    use crate::apis::{Error, ResponseContent};
78
79    fn response_error(content: &str) -> Error<()> {
80        Error::ResponseError(ResponseContent {
81            status:  reqwest::StatusCode::BAD_REQUEST,
82            content: content.to_string(),
83            entity:  None
84        })
85    }
86
87    #[test]
88    fn parses_a_single_message_envelope() {
89        let error = response_error(
90            r#"{
91                "status_code": 400,
92                "error_code": "bad_request",
93                "message": "invalid preset",
94                "response_id": "3037b284-a6ac-4dc7-b6d7-1f624dcfcec6"
95            }"#
96        );
97        let details = ErrorDetails::from_api_error(&error).expect("envelope parses");
98        assert_eq!(details.status_code, 400);
99        assert_eq!(details.error_code, "bad_request");
100        assert_eq!(details.messages(), vec!["invalid preset".to_string()]);
101        assert_eq!(
102            details.response_id.as_deref(),
103            Some("3037b284-a6ac-4dc7-b6d7-1f624dcfcec6")
104        );
105    }
106
107    #[test]
108    fn parses_a_message_list_envelope() {
109        let error = response_error(
110            r#"{"status_code": 400, "error_code": "bad_request", "message": ["a", "b"]}"#
111        );
112        let details = ErrorDetails::from_api_error(&error).expect("envelope parses");
113        assert_eq!(
114            details.message,
115            Some(ErrorMessage::Many(vec!["a".to_string(), "b".to_string()]))
116        );
117        assert_eq!(details.messages(), vec!["a".to_string(), "b".to_string()]);
118    }
119
120    #[test]
121    fn tolerates_a_missing_message() {
122        let error = response_error(r#"{"status_code": 500, "error_code": "internal"}"#);
123        let details = ErrorDetails::from_api_error(&error).expect("envelope parses");
124        assert!(details.message.is_none());
125        assert!(details.messages().is_empty());
126    }
127
128    #[test]
129    fn returns_none_for_non_envelope_bodies_and_other_errors() {
130        assert!(ErrorDetails::from_api_error(&response_error("<html>oops</html>")).is_none());
131        let serde_error: Error<()> =
132            Error::Serde(serde_json::from_str::<u32>("x").expect_err("must fail"));
133        assert!(ErrorDetails::from_api_error(&serde_error).is_none());
134    }
135}