Skip to main content

ferogram_mtsender/
errors.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2//
3// ferogram: async Telegram MTProto client in Rust
4// https://github.com/ankit-chaubey/ferogram
5//
6// Licensed under either the MIT License or the Apache License 2.0.
7// See the LICENSE-MIT or LICENSE-APACHE file in this repository:
8// https://github.com/ankit-chaubey/ferogram
9//
10// Feel free to use, modify, and share this code.
11// Please keep this notice when redistributing.
12
13use std::{fmt, io};
14
15/// An error returned by Telegram's servers in response to an RPC call.
16///
17/// Numeric values are stripped from the name and placed in [`RpcError::value`].
18///
19/// # Example
20/// `FLOOD_WAIT_30` → `RpcError { code: 420, name: "FLOOD_WAIT", value: Some(30) }`
21#[derive(Clone, Debug, PartialEq)]
22pub struct RpcError {
23    /// HTTP-like status code.
24    pub code: i32,
25    /// Error name in SCREAMING_SNAKE_CASE with digits removed.
26    pub name: String,
27    /// Numeric suffix extracted from the name, if any.
28    pub value: Option<u32>,
29}
30
31impl fmt::Display for RpcError {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(f, "RPC {}: {}", self.code, self.name)?;
34        if let Some(v) = self.value {
35            write!(f, " (value: {v})")?;
36        }
37        Ok(())
38    }
39}
40
41impl std::error::Error for RpcError {}
42
43impl RpcError {
44    /// Parse a raw Telegram error message like `"FLOOD_WAIT_30"` into an `RpcError`.
45    pub fn from_telegram(code: i32, message: &str) -> Self {
46        if let Some(idx) = message.rfind('_') {
47            let suffix = &message[idx + 1..];
48            if !suffix.is_empty()
49                && suffix.chars().all(|c| c.is_ascii_digit())
50                && let Ok(v) = suffix.parse::<u32>()
51            {
52                let name = message[..idx].to_string();
53                return Self {
54                    code,
55                    name,
56                    value: Some(v),
57                };
58            }
59        }
60        Self {
61            code,
62            name: message.to_string(),
63            value: None,
64        }
65    }
66
67    /// Match on the error name, with optional wildcard prefix/suffix `'*'`.
68    pub fn is(&self, pattern: &str) -> bool {
69        if let Some(prefix) = pattern.strip_suffix('*') {
70            self.name.starts_with(prefix)
71        } else if let Some(suffix) = pattern.strip_prefix('*') {
72            self.name.ends_with(suffix)
73        } else {
74            self.name == pattern
75        }
76    }
77
78    /// Returns the flood-wait duration in seconds, if this is a FLOOD_WAIT error.
79    pub fn flood_wait_seconds(&self) -> Option<u64> {
80        if self.code == 420 && self.name == "FLOOD_WAIT" {
81            self.value.map(|v| v as u64)
82        } else {
83            None
84        }
85    }
86
87    /// If this is a DC-migration redirect (code 303), returns the target DC id.
88    pub fn migrate_dc_id(&self) -> Option<i32> {
89        if self.code != 303 {
90            return None;
91        }
92        let is_migrate = self.name == "PHONE_MIGRATE"
93            || self.name == "NETWORK_MIGRATE"
94            || self.name == "FILE_MIGRATE"
95            || self.name == "USER_MIGRATE"
96            || self.name.ends_with("_MIGRATE");
97        if is_migrate {
98            Some(self.value.unwrap_or(2) as i32)
99        } else {
100            None
101        }
102    }
103}
104
105/// The error type returned from any `Client` method that talks to Telegram.
106#[derive(Debug)]
107#[non_exhaustive]
108pub enum InvocationError {
109    /// Telegram rejected the request.
110    Rpc(RpcError),
111    /// Network / I/O failure.
112    Io(io::Error),
113    /// Response deserialization failed.
114    Deserialize(String),
115    /// The request was dropped (e.g. sender task shut down).
116    Dropped,
117    /// DC migration required: handled internally by the client.
118    #[doc(hidden)]
119    Migrate(i32),
120    /// The cached access hash was rejected by Telegram.
121    StaleHash,
122    /// No access hash is cached for this peer.
123    PeerNotCached(String),
124}
125
126impl fmt::Display for InvocationError {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            Self::Rpc(e) => write!(f, "{e}"),
130            Self::Io(e) => write!(f, "I/O error: {e}"),
131            Self::Deserialize(s) => write!(f, "deserialize error: {s}"),
132            Self::Dropped => write!(f, "request dropped"),
133            Self::Migrate(dc) => write!(f, "DC migration to {dc}"),
134            Self::StaleHash => write!(f, "stale access hash; peer cache cleared, retry"),
135            Self::PeerNotCached(s) => write!(f, "peer not cached: {s}"),
136        }
137    }
138}
139
140impl std::error::Error for InvocationError {}
141
142impl From<io::Error> for InvocationError {
143    fn from(e: io::Error) -> Self {
144        Self::Io(e)
145    }
146}
147
148impl From<ferogram_tl_types::deserialize::Error> for InvocationError {
149    fn from(e: ferogram_tl_types::deserialize::Error) -> Self {
150        Self::Deserialize(e.to_string())
151    }
152}
153
154impl From<ferogram_connect::ConnectError> for InvocationError {
155    fn from(e: ferogram_connect::ConnectError) -> Self {
156        use ferogram_connect::ConnectError;
157        match e {
158            ConnectError::Io(e) => Self::Io(e),
159            ConnectError::Other(s) => Self::Deserialize(s),
160            ConnectError::TransportCode(code) => {
161                Self::Rpc(RpcError::from_telegram(code, "transport error"))
162            }
163            ConnectError::Rpc { code, message } => {
164                Self::Rpc(RpcError::from_telegram(code, &message))
165            }
166        }
167    }
168}
169
170impl InvocationError {
171    /// Returns `true` if this is the named RPC error (supports `'*'` wildcards).
172    pub fn is(&self, pattern: &str) -> bool {
173        match self {
174            Self::Rpc(e) => e.is(pattern),
175            _ => false,
176        }
177    }
178
179    /// If this is a FLOOD_WAIT error, returns how many seconds to wait.
180    pub fn flood_wait_seconds(&self) -> Option<u64> {
181        match self {
182            Self::Rpc(e) => e.flood_wait_seconds(),
183            _ => None,
184        }
185    }
186
187    /// If this error is a DC-migration redirect, returns the target DC id.
188    pub fn migrate_dc_id(&self) -> Option<i32> {
189        match self {
190            Self::Rpc(r) => r.migrate_dc_id(),
191            _ => None,
192        }
193    }
194}