serenity/http/
error.rs

1use std::error::Error as StdError;
2use std::fmt;
3
4use reqwest::header::InvalidHeaderValue;
5use reqwest::{Error as ReqwestError, Method, Response, StatusCode};
6use serde::de::{Deserialize, Deserializer, Error as _};
7use url::ParseError as UrlError;
8
9use crate::internal::prelude::*;
10use crate::json::*;
11
12#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
13#[non_exhaustive]
14pub struct DiscordJsonError {
15    /// The error code.
16    pub code: isize,
17    /// The error message.
18    pub message: String,
19    /// The full explained errors with their path in the request body.
20    #[serde(default, deserialize_with = "deserialize_errors")]
21    pub errors: Vec<DiscordJsonSingleError>,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
25pub struct DiscordJsonSingleError {
26    /// The error code.
27    pub code: String,
28    /// The error message.
29    pub message: String,
30    /// The path to the error in the request body itself, dot separated.
31    pub path: String,
32}
33
34#[derive(Clone, Debug, Eq, PartialEq)]
35#[non_exhaustive]
36pub struct ErrorResponse {
37    pub status_code: StatusCode,
38    pub url: String,
39    pub method: Method,
40    pub error: DiscordJsonError,
41}
42
43impl ErrorResponse {
44    // We need a freestanding from-function since we cannot implement an async From-trait.
45    pub async fn from_response(r: Response, method: Method) -> Self {
46        ErrorResponse {
47            status_code: r.status(),
48            url: r.url().to_string(),
49            method,
50            error: decode_resp(r).await.unwrap_or_else(|e| DiscordJsonError {
51                code: -1,
52                message: format!("[Serenity] Could not decode json when receiving error response from discord:, {e}"),
53                errors: vec![],
54            }),
55        }
56    }
57}
58
59#[derive(Debug)]
60#[non_exhaustive]
61pub enum HttpError {
62    /// When a non-successful status code was received for a request.
63    UnsuccessfulRequest(ErrorResponse),
64    /// When the decoding of a ratelimit header could not be properly decoded into an `i64` or
65    /// `f64`.
66    RateLimitI64F64,
67    /// When the decoding of a ratelimit header could not be properly decoded from UTF-8.
68    RateLimitUtf8,
69    /// When parsing an URL failed due to invalid input.
70    Url(UrlError),
71    /// When parsing a Webhook fails due to invalid input.
72    InvalidWebhook,
73    /// Header value contains invalid input.
74    InvalidHeader(InvalidHeaderValue),
75    /// Reqwest's Error contain information on why sending a request failed.
76    Request(ReqwestError),
77    /// When using a proxy with an invalid scheme.
78    InvalidScheme,
79    /// When using a proxy with an invalid port.
80    InvalidPort,
81    /// When an application id was expected but missing.
82    ApplicationIdMissing,
83}
84
85impl HttpError {
86    /// Returns true when the error is caused by an unsuccessful request
87    #[must_use]
88    pub fn is_unsuccessful_request(&self) -> bool {
89        matches!(self, Self::UnsuccessfulRequest(_))
90    }
91
92    /// Returns true when the error is caused by the url containing invalid input
93    #[must_use]
94    pub fn is_url_error(&self) -> bool {
95        matches!(self, Self::Url(_))
96    }
97
98    /// Returns true when the error is caused by an invalid header
99    #[must_use]
100    pub fn is_invalid_header(&self) -> bool {
101        matches!(self, Self::InvalidHeader(_))
102    }
103
104    /// Returns the status code if the error is an unsuccessful request
105    #[must_use]
106    pub fn status_code(&self) -> Option<StatusCode> {
107        match self {
108            Self::UnsuccessfulRequest(res) => Some(res.status_code),
109            _ => None,
110        }
111    }
112}
113
114impl From<ErrorResponse> for HttpError {
115    fn from(error: ErrorResponse) -> Self {
116        Self::UnsuccessfulRequest(error)
117    }
118}
119
120impl From<ReqwestError> for HttpError {
121    fn from(error: ReqwestError) -> Self {
122        Self::Request(error)
123    }
124}
125
126impl From<UrlError> for HttpError {
127    fn from(error: UrlError) -> Self {
128        Self::Url(error)
129    }
130}
131
132impl From<InvalidHeaderValue> for HttpError {
133    fn from(error: InvalidHeaderValue) -> Self {
134        Self::InvalidHeader(error)
135    }
136}
137
138impl fmt::Display for HttpError {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            Self::UnsuccessfulRequest(e) => {
142                f.write_str(&e.error.message)?;
143
144                // Put Discord's human readable error explanations in parentheses
145                let mut errors_iter = e.error.errors.iter();
146                if let Some(error) = errors_iter.next() {
147                    f.write_str(" (")?;
148                    f.write_str(&error.path)?;
149                    f.write_str(": ")?;
150                    f.write_str(&error.message)?;
151                    for error in errors_iter {
152                        f.write_str(", ")?;
153                        f.write_str(&error.path)?;
154                        f.write_str(": ")?;
155                        f.write_str(&error.message)?;
156                    }
157                    f.write_str(")")?;
158                }
159
160                Ok(())
161            },
162            Self::RateLimitI64F64 => f.write_str("Error decoding a header into an i64 or f64"),
163            Self::RateLimitUtf8 => f.write_str("Error decoding a header from UTF-8"),
164            Self::Url(_) => f.write_str("Provided URL is incorrect."),
165            Self::InvalidWebhook => f.write_str("Provided URL is not a valid webhook."),
166            Self::InvalidHeader(_) => f.write_str("Provided value is an invalid header value."),
167            Self::Request(_) => f.write_str("Error while sending HTTP request."),
168            Self::InvalidScheme => f.write_str("Invalid Url scheme."),
169            Self::InvalidPort => f.write_str("Invalid port."),
170            Self::ApplicationIdMissing => f.write_str("Application id was expected but missing."),
171        }
172    }
173}
174
175impl StdError for HttpError {
176    fn source(&self) -> Option<&(dyn StdError + 'static)> {
177        match self {
178            Self::Url(inner) => Some(inner),
179            Self::Request(inner) => Some(inner),
180            _ => None,
181        }
182    }
183}
184
185#[allow(clippy::missing_errors_doc)]
186pub fn deserialize_errors<'de, D: Deserializer<'de>>(
187    deserializer: D,
188) -> StdResult<Vec<DiscordJsonSingleError>, D::Error> {
189    let map: Value = Value::deserialize(deserializer)?;
190
191    if !map.is_object() {
192        return Ok(vec![]);
193    }
194
195    let mut errors = Vec::new();
196    let mut path = Vec::new();
197    loop_errors(&map, &mut errors, &mut path).map_err(D::Error::custom)?;
198
199    Ok(errors)
200}
201
202fn make_error(
203    errors_value: &Value,
204    errors: &mut Vec<DiscordJsonSingleError>,
205    path: &[&str],
206) -> StdResult<(), &'static str> {
207    let found_errors = errors_value.as_array().ok_or("expected array")?;
208
209    for error in found_errors {
210        let error_object = error.as_object().ok_or("expected object")?;
211
212        errors.push(DiscordJsonSingleError {
213            code: error_object
214                .get("code")
215                .ok_or("expected code")?
216                .as_str()
217                .ok_or("expected string")?
218                .to_owned(),
219            message: error_object
220                .get("message")
221                .ok_or("expected message")?
222                .as_str()
223                .ok_or("expected string")?
224                .to_owned(),
225            path: path.join("."),
226        });
227    }
228    Ok(())
229}
230
231fn loop_errors<'a>(
232    value: &'a Value,
233    errors: &mut Vec<DiscordJsonSingleError>,
234    path: &mut Vec<&'a str>,
235) -> StdResult<(), &'static str> {
236    for (key, value) in value.as_object().ok_or("expected object")? {
237        if key == "_errors" {
238            make_error(value, errors, path)?;
239        } else {
240            path.push(key);
241            loop_errors(value, errors, path)?;
242            path.pop();
243        }
244    }
245    Ok(())
246}
247
248#[cfg(test)]
249mod test {
250    use http_crate::response::Builder;
251    use reqwest::ResponseBuilderExt;
252
253    use super::*;
254
255    #[tokio::test]
256    async fn test_error_response_into() {
257        let error = DiscordJsonError {
258            code: 43121215,
259            message: String::from("This is a Ferris error"),
260            errors: vec![],
261        };
262
263        let mut builder = Builder::new();
264        builder = builder.status(403);
265        builder = builder.url(String::from("https://ferris.crab").parse().unwrap());
266        let body_string = to_string(&error).unwrap();
267        let response = builder.body(body_string.into_bytes()).unwrap();
268
269        let reqwest_response: reqwest::Response = response.into();
270        let error_response = ErrorResponse::from_response(reqwest_response, Method::POST).await;
271
272        let known = ErrorResponse {
273            status_code: reqwest::StatusCode::from_u16(403).unwrap(),
274            url: String::from("https://ferris.crab/"),
275            method: Method::POST,
276            error,
277        };
278
279        assert_eq!(error_response, known);
280    }
281}