Skip to main content

ferogram_mtsender/
errors.rs

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