azure_core/error/
http_error.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
use crate::{
    content_type, from_json,
    headers::{self, Headers},
    Response, StatusCode,
};
use bytes::Bytes;
use serde::Deserialize;

/// An unsuccessful HTTP response
#[derive(Debug)]
pub struct HttpError {
    status: StatusCode,
    details: ErrorDetails,
    headers: Headers,
    body: Bytes,
}

impl HttpError {
    /// Create an error from an http response.
    ///
    /// This does not check whether the response was a success and should only be used with unsuccessful responses.
    pub async fn new(response: Response) -> Self {
        let (status, headers, body) = response.deconstruct();
        let body = body
            .collect()
            .await
            .unwrap_or_else(|_| Bytes::from_static(b"<ERROR COLLECTING BODY>"));
        let details = ErrorDetails::new(&headers, &body);
        HttpError {
            status,
            details,
            headers,
            body,
        }
    }

    /// Get the status code for the http error
    pub fn status(&self) -> StatusCode {
        self.status
    }

    /// Get a reference to the http error's error code.
    pub fn error_code(&self) -> Option<&str> {
        self.details.code.as_deref()
    }

    /// Get a reference to the http error's error message.
    pub fn error_message(&self) -> Option<&str> {
        self.details.message.as_deref()
    }
}

impl std::fmt::Display for HttpError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let newline = if f.alternate() { "\n" } else { " " };
        let tab = if f.alternate() { "\t" } else { " " };
        write!(f, "HttpError {{{newline}")?;
        write!(f, "{tab}Status: {},{newline}", self.status)?;
        write!(
            f,
            "{tab}Error Code: {},{newline}",
            self.details
                .code
                .as_deref()
                .unwrap_or("<unknown error code>")
        )?;
        // TODO: sanitize body
        write!(f, "{tab}Body: \"{:?}\",{newline}", self.body)?;
        write!(f, "{tab}Headers: [{newline}")?;
        // TODO: sanitize headers
        for (k, v) in self.headers.iter() {
            write!(
                f,
                "{tab}{tab}{k}:{v}{newline}",
                k = k.as_str(),
                v = v.as_str()
            )?;
        }
        write!(f, "{tab}],{newline}}}{newline}")?;
        Ok(())
    }
}

impl std::error::Error for HttpError {}

#[derive(Debug)]
struct ErrorDetails {
    code: Option<String>,
    message: Option<String>,
}

impl ErrorDetails {
    fn new(headers: &Headers, body: &[u8]) -> Self {
        let header_err_code = get_error_code_from_header(headers);
        let content_type = headers.get_optional_str(&headers::CONTENT_TYPE);
        let (body_err_code, body_err_message) =
            get_error_code_message_from_body(body, content_type);

        let code = header_err_code.or(body_err_code);
        Self {
            code,
            message: body_err_message,
        }
    }
}

/// Gets the error code if it's present in the headers
///
/// For more info, see [here](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors)
pub(crate) fn get_error_code_from_header(headers: &Headers) -> Option<String> {
    headers.get_optional_string(&headers::ERROR_CODE)
}

#[derive(Deserialize)]
struct NestedError {
    #[serde(alias = "Message")]
    message: Option<String>,
    #[serde(alias = "Code")]
    code: Option<String>,
}

/// Error from a response body, aliases are set because XML responses follow different case-ing
#[derive(Deserialize)]
struct ErrorBody {
    #[serde(alias = "Error")]
    error: Option<NestedError>,
    #[serde(alias = "Message")]
    message: Option<String>,
    #[serde(alias = "Code")]
    code: Option<String>,
}

impl ErrorBody {
    /// Deconstructs self into error (code, message)
    ///
    /// The nested errors fields take precedence over those in the root of the structure
    fn into_code_message(self) -> (Option<String>, Option<String>) {
        let (nested_code, nested_message) = self
            .error
            .map(|nested_error| (nested_error.code, nested_error.message))
            .unwrap_or((None, None));
        (nested_code.or(self.code), nested_message.or(self.message))
    }
}

/// Gets the error code and message from the body based on the specified content_type
/// Support for xml decoding is dependent on the 'xml' feature flag
///
/// Assumes JSON if unspecified/inconclusive to maintain old behaviour
/// [#1275](https://github.com/Azure/azure-sdk-for-rust/issues/1275)
/// For more info, see [here](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md#handling-errors)
pub(crate) fn get_error_code_message_from_body(
    body: &[u8],
    content_type: Option<&str>,
) -> (Option<String>, Option<String>) {
    let err_body: Option<ErrorBody> = if content_type
        .is_some_and(|ctype| ctype == content_type::APPLICATION_XML.as_str())
    {
        #[cfg(feature = "xml")]
        {
            crate::xml::read_xml(body).ok()
        }
        #[cfg(not(feature = "xml"))]
        {
            tracing::warn!("encountered XML response but the 'xml' feature flag was not specified");
            None
        }
    } else {
        // keep old default of assuming JSON
        from_json(body).ok()
    };

    err_body
        .map(ErrorBody::into_code_message)
        .unwrap_or((None, None))
}