use std::time::Duration;
use http::StatusCode;
use thiserror::Error;
use crate::types::common::ResponseParameters;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ErrorClass {
Configuration,
Validation,
Authentication,
RateLimited,
Transport,
Storage,
Runtime,
Api,
Decode,
Protocol,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid base url `{input}`: {source}")]
InvalidBaseUrl {
input: String,
#[source]
source: url::ParseError,
},
#[error("base url must use http or https, got `{scheme}`")]
InvalidBaseUrlScheme { scheme: String },
#[error("invalid Telegram method name `{method}`")]
InvalidMethodName { method: String },
#[error("invalid Telegram bot token")]
InvalidBotToken,
#[error("missing bot token authentication")]
MissingBotToken,
#[error("authentication failed: {reason}")]
Authentication { reason: String },
#[error("invalid request: {reason}")]
InvalidRequest { reason: String },
#[error("invalid client configuration: {reason}")]
Configuration { reason: String },
#[error("invalid default header name `{name}`: {source}")]
InvalidHeaderName {
name: String,
#[source]
source: http::header::InvalidHeaderName,
},
#[error("invalid default header value for `{name}`: {source}")]
InvalidHeaderValue {
name: String,
#[source]
source: http::header::InvalidHeaderValue,
},
#[error("failed to serialize request body: {source}")]
SerializeRequest {
#[source]
source: serde_json::Error,
},
#[error("failed to read local file `{path}`: {source}")]
ReadLocalFile {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to deserialize Telegram response for `{method}`")]
DeserializeResponse {
method: String,
status: Option<u16>,
request_id: Option<Box<str>>,
body_snippet: Option<Box<str>>,
#[source]
source: serde_json::Error,
},
#[error("transport error while calling `{method}`: {message}")]
Transport {
method: String,
status: Option<u16>,
request_id: Option<Box<str>>,
retry_after: Option<Duration>,
request_path: Option<Box<str>>,
message: Box<str>,
},
#[error("storage error during `{operation}`: {message}")]
Storage {
operation: Box<str>,
message: Box<str>,
retryable: bool,
},
#[error("runtime error: {reason}")]
Runtime { reason: String },
#[error("telegram api error while calling `{method}`: {description}")]
Api {
method: String,
status: Option<u16>,
request_id: Option<Box<str>>,
error_code: Option<i64>,
description: Box<str>,
retry_after: Option<Duration>,
parameters: Option<Box<ResponseParameters>>,
body_snippet: Option<Box<str>>,
},
#[error("telegram api returned `ok=true` without `result` for `{method}`")]
MissingResult {
method: String,
status: Option<u16>,
request_id: Option<Box<str>>,
body_snippet: Option<Box<str>>,
},
}
impl Error {
pub fn classification(&self) -> ErrorClass {
match self {
Self::InvalidBaseUrl { .. }
| Self::InvalidBaseUrlScheme { .. }
| Self::InvalidMethodName { .. }
| Self::InvalidHeaderName { .. }
| Self::InvalidHeaderValue { .. }
| Self::Configuration { .. } => ErrorClass::Configuration,
Self::InvalidBotToken | Self::MissingBotToken | Self::Authentication { .. } => {
ErrorClass::Authentication
}
Self::InvalidRequest { .. }
| Self::SerializeRequest { .. }
| Self::ReadLocalFile { .. } => ErrorClass::Validation,
Self::DeserializeResponse { .. } => ErrorClass::Decode,
Self::MissingResult { .. } => ErrorClass::Protocol,
Self::Storage { .. } => ErrorClass::Storage,
Self::Runtime { .. } => ErrorClass::Runtime,
Self::Transport { status, .. } => {
if *status == Some(429) {
return ErrorClass::RateLimited;
}
if matches!(*status, Some(401 | 403)) {
return ErrorClass::Authentication;
}
ErrorClass::Transport
}
Self::Api {
status,
error_code,
parameters,
..
} => {
if status.is_some_and(|code| code == 401 || code == 403)
|| error_code.is_some_and(|code| code == 401 || code == 403)
{
return ErrorClass::Authentication;
}
if status.is_some_and(|code| code == 429)
|| error_code.is_some_and(|code| code == 429)
|| parameters
.as_deref()
.and_then(|parameters| parameters.retry_after)
.is_some()
{
return ErrorClass::RateLimited;
}
ErrorClass::Api
}
}
}
pub fn status(&self) -> Option<StatusCode> {
let code = match self {
Self::DeserializeResponse { status, .. }
| Self::Transport { status, .. }
| Self::Api { status, .. }
| Self::MissingResult { status, .. } => *status,
_ => None,
}?;
StatusCode::from_u16(code).ok()
}
pub fn request_id(&self) -> Option<&str> {
match self {
Self::DeserializeResponse { request_id, .. }
| Self::Transport { request_id, .. }
| Self::Api { request_id, .. }
| Self::MissingResult { request_id, .. } => request_id.as_deref(),
_ => None,
}
}
pub fn error_code(&self) -> Option<i64> {
match self {
Self::Api { error_code, .. } => *error_code,
_ => None,
}
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
Self::Transport { retry_after, .. } => *retry_after,
Self::Api {
parameters,
retry_after,
..
} => parameters
.as_deref()
.and_then(|parameters| parameters.retry_after)
.map(Duration::from_secs)
.or(*retry_after),
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
match self.classification() {
ErrorClass::RateLimited => true,
ErrorClass::Authentication
| ErrorClass::Configuration
| ErrorClass::Validation
| ErrorClass::Runtime
| ErrorClass::Decode
| ErrorClass::Protocol => false,
ErrorClass::Storage => matches!(
self,
Self::Storage {
retryable: true,
..
}
),
ErrorClass::Transport => match self {
Self::Transport { status: None, .. } => true,
Self::Transport {
status: Some(status),
..
} => is_retryable_status_code(*status),
_ => false,
},
ErrorClass::Api => match self {
Self::Api {
status, error_code, ..
} => {
status.is_some_and(is_retryable_status_code)
|| error_code
.and_then(error_code_as_status_code)
.is_some_and(is_retryable_status_code)
}
_ => false,
},
}
}
pub fn is_rate_limited(&self) -> bool {
self.classification() == ErrorClass::RateLimited
}
pub fn is_auth_error(&self) -> bool {
self.classification() == ErrorClass::Authentication
}
}
fn is_retryable_status_code(status: u16) -> bool {
matches!(status, 408 | 409 | 425 | 429 | 500 | 502 | 503 | 504)
}
fn error_code_as_status_code(error_code: i64) -> Option<u16> {
let status = u16::try_from(error_code).ok()?;
StatusCode::from_u16(status).ok()?;
Some(status)
}