twilight_http/
error.rs

1use crate::{api_error::ApiError, json::JsonError, response::StatusCode};
2use http::Response;
3use hyper::body::Incoming;
4use std::{
5    error::Error as StdError,
6    fmt::{Debug, Display, Formatter, Result as FmtResult},
7    str,
8};
9
10#[derive(Debug)]
11pub struct Error {
12    pub(super) source: Option<Box<dyn StdError + Send + Sync>>,
13    pub(super) kind: ErrorType,
14}
15
16impl Error {
17    /// Immutable reference to the type of error that occurred.
18    #[must_use = "retrieving the type has no effect if left unused"]
19    pub const fn kind(&self) -> &ErrorType {
20        &self.kind
21    }
22
23    /// Consume the error, returning the source error if there is any.
24    #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
25    pub fn into_source(self) -> Option<Box<dyn StdError + Send + Sync>> {
26        self.source
27    }
28
29    /// Consume the error, returning the owned error type and the source error.
30    #[must_use = "consuming the error into its parts has no effect if left unused"]
31    pub fn into_parts(self) -> (ErrorType, Option<Box<dyn StdError + Send + Sync>>) {
32        (self.kind, self.source)
33    }
34
35    pub(super) fn json(source: JsonError) -> Self {
36        Self {
37            kind: ErrorType::Json,
38            source: Some(Box::new(source)),
39        }
40    }
41
42    pub(super) fn validation(source: impl StdError + Send + Sync + 'static) -> Self {
43        Self {
44            kind: ErrorType::Validation,
45            source: Some(Box::new(source)),
46        }
47    }
48}
49
50impl Display for Error {
51    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
52        match &self.kind {
53            ErrorType::BuildingRequest => f.write_str("failed to build the request"),
54            ErrorType::ChunkingResponse => f.write_str("Chunking the response failed"),
55            ErrorType::CreatingHeader { name, .. } => {
56                f.write_str("Parsing the value for header {}")?;
57                f.write_str(name)?;
58
59                f.write_str(" failed")
60            }
61            ErrorType::Json => f.write_str("Given value couldn't be serialized"),
62            ErrorType::Parsing { body, .. } => {
63                f.write_str("Response body couldn't be deserialized: ")?;
64
65                if let Ok(body) = str::from_utf8(body) {
66                    f.write_str(body)
67                } else {
68                    Debug::fmt(body, f)
69                }
70            }
71            ErrorType::RatelimiterTicket => f.write_str("Failed to get ratelimiter ticket"),
72            ErrorType::RequestCanceled => {
73                f.write_str("Request was canceled either before or while being sent")
74            }
75            ErrorType::RequestError => f.write_str("Parsing or sending the response failed"),
76            ErrorType::RequestTimedOut => f.write_str("request timed out"),
77            ErrorType::Response { body, status, .. } => {
78                f.write_str("Response error: status code ")?;
79                Display::fmt(status, f)?;
80                f.write_str(", error: ")?;
81
82                f.write_str(&String::from_utf8_lossy(body))
83            }
84            ErrorType::ServiceUnavailable { .. } => {
85                f.write_str("api may be temporarily unavailable (received a 503)")
86            }
87            ErrorType::Unauthorized => {
88                f.write_str("token in use is invalid, expired, or is revoked")
89            }
90            ErrorType::Validation => f.write_str("request fields have invalid values"),
91        }
92    }
93}
94
95impl StdError for Error {
96    fn source(&self) -> Option<&(dyn StdError + 'static)> {
97        self.source
98            .as_ref()
99            .map(|source| &**source as &(dyn StdError + 'static))
100    }
101}
102
103/// Type of [`Error`] that occurred.
104#[non_exhaustive]
105pub enum ErrorType {
106    BuildingRequest,
107    ChunkingResponse,
108    CreatingHeader {
109        name: String,
110    },
111    Json,
112    Parsing {
113        body: Vec<u8>,
114    },
115    RatelimiterTicket,
116    RequestCanceled,
117    RequestError,
118    RequestTimedOut,
119    Response {
120        body: Vec<u8>,
121        error: ApiError,
122        status: StatusCode,
123    },
124    /// API service is unavailable. Consider re-sending the request at a
125    /// later time.
126    ///
127    /// This may occur during Discord API stability incidents.
128    ServiceUnavailable {
129        response: Response<Incoming>,
130    },
131    /// Token in use has become revoked or is otherwise invalid.
132    ///
133    /// This can occur if a bot token is invalidated or an access token expires
134    /// or is revoked. Recreate the client to configure a new token.
135    Unauthorized,
136    /// A field failed validation requirements during request building.
137    ///
138    /// The inputs of request methods for fields are validated for correctness.
139    /// For example, [`CreateMessage::content`] is validated to ensure that the
140    /// message content isn't too long; [`ExecuteWebhook::embeds`] is validated
141    /// to ensure that a correct number of embeds are provided; and so on.
142    ///
143    /// Validation failures aren't immediately returned; rather, validation
144    /// errors are returned when calling the [`IntoFuture`] or
145    /// [`TryIntoRequest`] implementations on requests.
146    ///
147    /// # Examples
148    ///
149    /// Passing a message with valid content succeeds as expected:
150    ///
151    /// ```no_run
152    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
153    /// # let channel_id = twilight_model::id::Id::new(1);
154    /// use std::env;
155    /// use twilight_http::{client::Client, request::TryIntoRequest};
156    ///
157    /// let client = Client::new(env::var("DISCORD_TOKEN")?);
158    /// let builder = client.create_message(channel_id).content("Ping!");
159    ///
160    /// assert!(builder.try_into_request().is_ok());
161    /// # Ok(()) }
162    /// ```
163    ///
164    /// However, passing an invalid content returns a validation error upon
165    /// finalizing the request building:
166    ///
167    /// ```no_run
168    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
169    /// # let channel_id = twilight_model::id::Id::new(1);
170    /// use std::{env, error::Error};
171    /// use twilight_http::{client::Client, error::ErrorType, request::TryIntoRequest};
172    ///
173    /// let client = Client::new(env::var("DISCORD_TOKEN")?);
174    ///
175    /// // this is a very long message
176    /// let content = "pinkie pie is cool ".repeat(1000);
177    ///
178    /// let builder = client.create_message(channel_id).content(&content);
179    ///
180    /// let error = builder.try_into_request().unwrap_err();
181    /// assert!(matches!(error.kind(), ErrorType::Validation));
182    ///
183    /// // print the contents of the validation error
184    /// println!("{:?}", error.source());
185    /// # Ok(()) }
186    /// ```
187    ///
188    /// [`CreateMessage::content`]: crate::request::channel::message::create_message::CreateMessage
189    /// [`ExecuteWebhook::embeds`]: crate::request::channel::webhook::execute_webhook::ExecuteWebhook
190    /// [`IntoFuture`]: std::future::IntoFuture
191    /// [`TryIntoRequest`]: crate::request::TryIntoRequest
192    Validation,
193}
194
195impl Debug for ErrorType {
196    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
197        match self {
198            Self::BuildingRequest => f.write_str("BuildingRequest"),
199            Self::ChunkingResponse => f.write_str("ChunkingResponse"),
200            Self::CreatingHeader { name } => f
201                .debug_struct("CreatingHeader")
202                .field("name", name)
203                .finish(),
204            Self::Json => f.write_str("Json"),
205            Self::Parsing { body } => {
206                let mut debug = f.debug_struct("Parsing");
207
208                if let Ok(body_string) = str::from_utf8(body) {
209                    debug.field("body_string", &body_string);
210                }
211
212                debug.field("body", body).finish()
213            }
214            Self::RatelimiterTicket => f.write_str("RatelimiterTicket"),
215            Self::RequestCanceled => f.write_str("RequestCanceled"),
216            Self::RequestError => f.write_str("RequestError"),
217            Self::RequestTimedOut => f.write_str("RequestTimedOut"),
218            Self::Response {
219                body,
220                error,
221                status,
222            } => {
223                let mut debug = f.debug_struct("Response");
224
225                if let Ok(body_string) = str::from_utf8(body) {
226                    debug.field("body_string", &body_string);
227                }
228
229                debug
230                    .field("body", body)
231                    .field("error", error)
232                    .field("status", status)
233                    .finish()
234            }
235            Self::ServiceUnavailable { response } => f
236                .debug_struct("ServiceUnavailable")
237                .field("response", response)
238                .finish(),
239            Self::Unauthorized => f.write_str("Unauthorized"),
240            Self::Validation => f.write_str("Validation"),
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::ErrorType;
248    use crate::{
249        api_error::{ApiError, GeneralApiError},
250        response::StatusCode,
251    };
252
253    /// Ensure
254    #[test]
255    fn parsing_variant_debug() {
256        let body = br#"{"message": "aaa"#.to_vec();
257
258        let error = ErrorType::Parsing { body };
259
260        assert_eq!(
261            "Parsing {
262    body_string: \"{\\\"message\\\": \\\"aaa\",
263    body: [
264        123,
265        34,
266        109,
267        101,
268        115,
269        115,
270        97,
271        103,
272        101,
273        34,
274        58,
275        32,
276        34,
277        97,
278        97,
279        97,
280    ],
281}",
282            format!("{error:#?}"),
283        );
284    }
285
286    #[test]
287    fn response_variant_debug() {
288        let body = br#"{"message": "aaa"}"#.to_vec();
289
290        let error = ErrorType::Response {
291            body,
292            error: ApiError::General(GeneralApiError {
293                code: 0,
294                message: "401: Unauthorized".to_owned(),
295            }),
296            status: StatusCode::new(401),
297        };
298
299        assert_eq!(
300            "Response {
301    body_string: \"{\\\"message\\\": \\\"aaa\\\"}\",
302    body: [
303        123,
304        34,
305        109,
306        101,
307        115,
308        115,
309        97,
310        103,
311        101,
312        34,
313        58,
314        32,
315        34,
316        97,
317        97,
318        97,
319        34,
320        125,
321    ],
322    error: General(
323        GeneralApiError {
324            code: 0,
325            message: \"401: Unauthorized\",
326        },
327    ),
328    status: StatusCode(
329        401,
330    ),
331}",
332            format!("{error:#?}"),
333        );
334    }
335}