azure_core/error/
http_error.rs

1use crate::{
2    content_type, from_json,
3    headers::{self, Headers},
4    Response, StatusCode,
5};
6use bytes::Bytes;
7use serde::Deserialize;
8
9/// An unsuccessful HTTP response
10#[derive(Debug)]
11pub struct HttpError {
12    status: StatusCode,
13    details: ErrorDetails,
14    headers: Headers,
15    body: Bytes,
16}
17
18impl HttpError {
19    /// Create an error from an http response.
20    ///
21    /// This does not check whether the response was a success and should only be used with unsuccessful responses.
22    pub async fn new(response: Response) -> Self {
23        let (status, headers, body) = response.deconstruct();
24        let body = body
25            .collect()
26            .await
27            .unwrap_or_else(|_| Bytes::from_static(b"<ERROR COLLECTING BODY>"));
28        let details = ErrorDetails::new(&headers, &body);
29        HttpError {
30            status,
31            details,
32            headers,
33            body,
34        }
35    }
36
37    /// Get the status code for the http error
38    pub fn status(&self) -> StatusCode {
39        self.status
40    }
41
42    /// Get a reference to the http error's error code.
43    pub fn error_code(&self) -> Option<&str> {
44        self.details.code.as_deref()
45    }
46
47    /// Get a reference to the http error's error message.
48    pub fn error_message(&self) -> Option<&str> {
49        self.details.message.as_deref()
50    }
51}
52
53impl std::fmt::Display for HttpError {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        let newline = if f.alternate() { "\n" } else { " " };
56        let tab = if f.alternate() { "\t" } else { " " };
57        write!(f, "HttpError {{{newline}")?;
58        write!(f, "{tab}Status: {},{newline}", self.status)?;
59        write!(
60            f,
61            "{tab}Error Code: {},{newline}",
62            self.details
63                .code
64                .as_deref()
65                .unwrap_or("<unknown error code>")
66        )?;
67        // TODO: sanitize body
68        write!(f, "{tab}Body: \"{:?}\",{newline}", self.body)?;
69        write!(f, "{tab}Headers: [{newline}")?;
70        // TODO: sanitize headers
71        for (k, v) in self.headers.iter() {
72            write!(
73                f,
74                "{tab}{tab}{k}:{v}{newline}",
75                k = k.as_str(),
76                v = v.as_str()
77            )?;
78        }
79        write!(f, "{tab}],{newline}}}{newline}")?;
80        Ok(())
81    }
82}
83
84impl std::error::Error for HttpError {}
85
86#[derive(Debug)]
87struct ErrorDetails {
88    code: Option<String>,
89    message: Option<String>,
90}
91
92impl ErrorDetails {
93    fn new(headers: &Headers, body: &[u8]) -> Self {
94        let header_err_code = get_error_code_from_header(headers);
95        let content_type = headers.get_optional_str(&headers::CONTENT_TYPE);
96        let (body_err_code, body_err_message) =
97            get_error_code_message_from_body(body, content_type);
98
99        let code = header_err_code.or(body_err_code);
100        Self {
101            code,
102            message: body_err_message,
103        }
104    }
105}
106
107/// Gets the error code if it's present in the headers
108///
109/// For more info, see [here](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors)
110pub(crate) fn get_error_code_from_header(headers: &Headers) -> Option<String> {
111    headers.get_optional_string(&headers::ERROR_CODE)
112}
113
114#[derive(Deserialize)]
115struct NestedError {
116    #[serde(alias = "Message")]
117    message: Option<String>,
118    #[serde(alias = "Code")]
119    code: Option<String>,
120}
121
122/// Error from a response body, aliases are set because XML responses follow different case-ing
123#[derive(Deserialize)]
124struct ErrorBody {
125    #[serde(alias = "Error")]
126    error: Option<NestedError>,
127    #[serde(alias = "Message")]
128    message: Option<String>,
129    #[serde(alias = "Code")]
130    code: Option<String>,
131}
132
133impl ErrorBody {
134    /// Deconstructs self into error (code, message)
135    ///
136    /// The nested errors fields take precedence over those in the root of the structure
137    fn into_code_message(self) -> (Option<String>, Option<String>) {
138        let (nested_code, nested_message) = self
139            .error
140            .map(|nested_error| (nested_error.code, nested_error.message))
141            .unwrap_or((None, None));
142        (nested_code.or(self.code), nested_message.or(self.message))
143    }
144}
145
146/// Gets the error code and message from the body based on the specified content_type
147/// Support for xml decoding is dependent on the 'xml' feature flag
148///
149/// Assumes JSON if unspecified/inconclusive to maintain old behaviour
150/// [#1275](https://github.com/Azure/azure-sdk-for-rust/issues/1275)
151/// For more info, see [here](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors)
152pub(crate) fn get_error_code_message_from_body(
153    body: &[u8],
154    content_type: Option<&str>,
155) -> (Option<String>, Option<String>) {
156    let err_body: Option<ErrorBody> = if content_type
157        .is_some_and(|ctype| ctype == content_type::APPLICATION_XML.as_str())
158    {
159        #[cfg(feature = "xml")]
160        {
161            crate::xml::read_xml(body).ok()
162        }
163        #[cfg(not(feature = "xml"))]
164        {
165            tracing::warn!("encountered XML response but the 'xml' feature flag was not specified");
166            None
167        }
168    } else {
169        // keep old default of assuming JSON
170        from_json(body).ok()
171    };
172
173    err_body
174        .map(ErrorBody::into_code_message)
175        .unwrap_or((None, None))
176}