Skip to main content

twilight_http/
error.rs

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