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