grammers_mtsender/
errors.rs

1// Copyright 2020 - developers of the `grammers` project.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8use grammers_mtproto::{authentication, mtp, transport};
9use grammers_tl_types as tl;
10use std::fmt;
11use std::io;
12
13/// This error occurs when reading from the network fails.
14#[derive(Debug)]
15pub enum ReadError {
16    /// Standard I/O error.
17    Io(io::Error),
18    /// Error propagated from the underlying [`transport`].
19    Transport(transport::Error),
20    /// Error propagated from attempting to deserialize an invalid [`tl::Deserializable`].
21    Deserialize(mtp::DeserializeError),
22}
23
24impl std::error::Error for ReadError {}
25
26impl Clone for ReadError {
27    fn clone(&self) -> Self {
28        match self {
29            Self::Io(e) => Self::Io(
30                e.raw_os_error()
31                    .map(io::Error::from_raw_os_error)
32                    .unwrap_or_else(|| io::Error::new(e.kind(), e.to_string())),
33            ),
34            Self::Transport(e) => Self::Transport(e.clone()),
35            Self::Deserialize(e) => Self::Deserialize(e.clone()),
36        }
37    }
38}
39
40impl fmt::Display for ReadError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::Io(err) => write!(f, "read error, IO failed: {err}"),
44            Self::Transport(err) => write!(f, "read error, transport-level: {err}"),
45            Self::Deserialize(err) => write!(f, "read error, bad response: {err}"),
46        }
47    }
48}
49
50impl From<io::Error> for ReadError {
51    fn from(error: io::Error) -> Self {
52        Self::Io(error)
53    }
54}
55
56impl From<transport::Error> for ReadError {
57    fn from(error: transport::Error) -> Self {
58        Self::Transport(error)
59    }
60}
61
62impl From<mtp::DeserializeError> for ReadError {
63    fn from(error: mtp::DeserializeError) -> Self {
64        Self::Deserialize(error)
65    }
66}
67
68impl From<tl::deserialize::Error> for ReadError {
69    fn from(error: tl::deserialize::Error) -> Self {
70        Self::Deserialize(error.into())
71    }
72}
73
74/// The error type reported by the server when a request is misused.
75///
76/// These are returned when Telegram respond to an RPC with [`tl::types::RpcError`].
77#[derive(Clone, Debug, PartialEq)]
78pub struct RpcError {
79    /// A numerical value similar to [HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status).
80    pub code: i32,
81
82    /// The ASCII error name, normally in screaming snake case.
83    ///
84    /// Digit words are removed from the name and put in the [`RpcError::value`] instead.
85    /// ```
86    /// use grammers_mtsender::RpcError;
87    /// let rpc_error = RpcError::from(grammers_tl_types::types::RpcError {
88    ///         error_code: 500, error_message: "INTERDC_2_CALL_ERROR".into() });
89    /// assert_eq!(rpc_error.name, "INTERDC_CALL_ERROR");
90    /// assert_eq!(rpc_error.value, Some(2));
91    /// ```
92    pub name: String,
93
94    /// If the error contained an additional integer value, it will be present here and removed from the [`RpcError::name`].
95    /// ```
96    /// use grammers_mtsender::RpcError;
97    /// let rpc_error = RpcError::from(grammers_tl_types::types::RpcError {
98    ///         error_code: 420, error_message: "FLOOD_WAIT_31".into() });
99    /// assert_eq!(rpc_error.name, "FLOOD_WAIT");
100    /// assert_eq!(rpc_error.value, Some(31));
101    /// ```
102    pub value: Option<u32>,
103
104    /// The constructor identifier of the request that triggered this error.
105    /// Won't be present if the error was artificially constructed.
106    pub caused_by: Option<u32>,
107}
108
109impl std::error::Error for RpcError {}
110
111impl fmt::Display for RpcError {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "rpc error {}: {}", self.code, self.name)?;
114        if let Some(caused_by) = self.caused_by {
115            write!(f, " caused by {}", tl::name_for_id(caused_by))?;
116        }
117        if let Some(value) = self.value {
118            write!(f, " (value: {value})")?;
119        }
120        Ok(())
121    }
122}
123
124impl From<tl::types::RpcError> for RpcError {
125    fn from(error: tl::types::RpcError) -> Self {
126        // Extract the numeric value in the error, if any
127        if let Some(value) = error
128            .error_message
129            .split(|c: char| !c.is_ascii_digit())
130            .find(|s| !s.is_empty())
131        {
132            let mut to_remove = String::with_capacity(1 + value.len());
133            to_remove.push('_');
134            to_remove.push_str(value);
135            Self {
136                code: error.error_code,
137                name: error.error_message.replace(&to_remove, ""),
138                // Safe to unwrap, matched on digits
139                value: Some(value.parse().unwrap()),
140                caused_by: None,
141            }
142        } else {
143            Self {
144                code: error.error_code,
145                name: error.error_message.clone(),
146                value: None,
147                caused_by: None,
148            }
149        }
150    }
151}
152
153impl RpcError {
154    /// Matches on the name of the RPC error (case-sensitive).
155    ///
156    /// Useful in `match` arm guards. A single trailing or leading asterisk (`'*'`) is allowed,
157    /// and will instead check if the error name starts (or ends with) the input parameter.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// # let request_result = Result::<(), _>::Err(grammers_mtsender::RpcError {
163    /// #     code: 400, name: "PHONE_CODE_INVALID".to_string(), value: None, caused_by: None });
164    /// #
165    /// match request_result {
166    ///     Err(rpc_err) if rpc_err.is("SESSION_PASSWORD_NEEDED") => panic!(),
167    ///     Err(rpc_err) if rpc_err.is("PHONE_CODE_*") => {},
168    ///     _ => panic!()
169    /// }
170    /// ```
171    pub fn is(&self, rpc_error: &str) -> bool {
172        if let Some(rpc_error) = rpc_error.strip_suffix('*') {
173            self.name.starts_with(rpc_error)
174        } else if let Some(rpc_error) = rpc_error.strip_prefix('*') {
175            self.name.ends_with(rpc_error)
176        } else {
177            self.name == rpc_error
178        }
179    }
180
181    /// Attaches the [`tl::Identifiable::CONSTRUCTOR_ID`] of the
182    /// request that caused this error to the error information.
183    pub fn with_caused_by(mut self, constructor_id: u32) -> Self {
184        self.caused_by = Some(constructor_id);
185        self
186    }
187}
188
189/// This error occurs when a Remote Procedure call was unsuccessful.
190#[derive(Debug)]
191pub enum InvocationError {
192    /// The request invocation failed because it was invalid or the server
193    /// could not process it successfully. If the server is suffering from
194    /// temporary issues, the request may be retried after some time.
195    Rpc(RpcError),
196
197    /// Standard I/O error when reading the response.
198    ///
199    /// Telegram may kill the connection at any moment, but it is generally valid to retry
200    /// the request at least once immediately, which will be done through a new connection.
201    Io(io::Error),
202
203    /// Error propagated from attempting to deserialize an invalid [`tl::Deserializable`].
204    ///
205    /// This occurs somewhat frequently when misusing a single session more than once at a time.
206    /// Otherwise it might happen on bleeding-edge layers that have not had time to settle yet.
207    Deserialize(mtp::DeserializeError),
208
209    /// Error propagated from the underlying [`transport`].
210    ///
211    /// The most common variant is [`transport::Error::BadStatus`], which can occur when
212    /// there's no valid Authorization Key (404) or too many connections have been made (429).
213    Transport(transport::Error),
214
215    /// The request was cancelled or dropped, and the results won't arrive.
216    /// This may mean that the [`crate::SenderPoolRunner`] is no longer running.
217    Dropped,
218
219    /// The request was invoked in a datacenter that does not exist or is not known by the session.
220    InvalidDc,
221
222    /// The request caused the sender to connect to a new datacenter to be performed,
223    /// but the Authorization Key generation process failed.
224    Authentication(authentication::Error),
225}
226
227impl std::error::Error for InvocationError {}
228
229impl fmt::Display for InvocationError {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self {
232            Self::Rpc(err) => write!(f, "request error: {err}"),
233            Self::Io(err) => write!(f, "request error: {err}"),
234            Self::Deserialize(err) => write!(f, "request error: {err}"),
235            Self::Transport(err) => write!(f, "request error: {err}"),
236            Self::Dropped => write!(f, "request error: dropped (cancelled)"),
237            Self::InvalidDc => write!(f, "request error: invalid dc"),
238            Self::Authentication(err) => write!(f, "request error: {err}"),
239        }
240    }
241}
242
243impl From<ReadError> for InvocationError {
244    fn from(error: ReadError) -> Self {
245        match error {
246            ReadError::Io(error) => Self::from(error),
247            ReadError::Transport(error) => Self::from(error),
248            ReadError::Deserialize(error) => Self::from(error),
249        }
250    }
251}
252
253impl From<mtp::DeserializeError> for InvocationError {
254    fn from(error: mtp::DeserializeError) -> Self {
255        Self::from(ReadError::from(error))
256    }
257}
258
259impl From<transport::Error> for InvocationError {
260    fn from(error: transport::Error) -> Self {
261        Self::from(ReadError::from(error))
262    }
263}
264
265impl From<tl::deserialize::Error> for InvocationError {
266    fn from(error: tl::deserialize::Error) -> Self {
267        Self::from(ReadError::from(error))
268    }
269}
270
271impl From<io::Error> for InvocationError {
272    fn from(error: io::Error) -> Self {
273        Self::from(ReadError::from(error))
274    }
275}
276
277impl From<authentication::Error> for InvocationError {
278    fn from(error: authentication::Error) -> Self {
279        Self::Authentication(error)
280    }
281}
282
283impl InvocationError {
284    /// Matches on the name of the RPC error (case-sensitive).
285    ///
286    /// Useful in `match` arm guards. A single trailing or leading asterisk (`'*'`) is allowed,
287    /// and will instead check if the error name starts (or ends with) the input parameter.
288    ///
289    /// If the error is not a RPC error, returns `false`.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// # let request_result = Result::<(), _>::Err(grammers_mtsender::InvocationError::Rpc(
295    /// #     grammers_mtsender::RpcError { code: 400, name: "PHONE_CODE_INVALID".to_string(), value: None, caused_by: None }));
296    /// #
297    /// match request_result {
298    ///     Err(err) if err.is("SESSION_PASSWORD_NEEDED") => panic!(),
299    ///     Err(err) if err.is("PHONE_CODE_*") => {},
300    ///     _ => panic!()
301    /// }
302    /// ```
303    #[inline]
304    pub fn is(&self, rpc_error: &str) -> bool {
305        match self {
306            Self::Rpc(rpc) => rpc.is(rpc_error),
307            _ => false,
308        }
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn check_rpc_error_parsing() {
318        assert_eq!(
319            RpcError::from(tl::types::RpcError {
320                error_code: 400,
321                error_message: "CHAT_INVALID".into(),
322            }),
323            RpcError {
324                code: 400,
325                name: "CHAT_INVALID".into(),
326                value: None,
327                caused_by: None,
328            }
329        );
330    }
331}