use std::time::{Duration, SystemTime, SystemTimeError};
use std::fmt;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpError {
pub status: u16,
pub message: Option<String>,
pub request_id: Option<String>,
pub body_snippet: Option<String>,
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "HTTP {}", self.status)?;
if let Some(message) = &self.message {
write!(f, ": {message}")?;
}
if let Some(request_id) = &self.request_id {
write!(f, " [request-id: {request_id}]")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransportError {
pub status: Option<u16>,
pub message: Option<String>,
pub request_id: Option<String>,
pub body_snippet: Option<String>,
pub retry_after: Option<Duration>,
pub retryable: bool,
pub code: &'static str,
pub method: Option<String>,
pub uri: Option<String>,
pub timeout_phase: Option<&'static str>,
pub transport_kind: Option<&'static str>,
}
impl fmt::Display for TransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(status) = self.status {
write!(f, "HTTP {status}")?;
} else {
write!(f, "request failed")?;
}
if let Some(message) = &self.message {
write!(f, ": {message}")?;
}
if let Some(request_id) = &self.request_id {
write!(f, " [request-id: {request_id}]")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
Auth,
NotFound,
Conflict,
RateLimited,
Api,
Transport,
Serialization,
Timestamp,
Signature,
InvalidConfig,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("API error (code={code}): {message}")]
Api {
code: i64,
message: String,
request_id: Option<String>,
body_snippet: Option<String>,
},
#[error("Authentication failed: {0}")]
Auth(HttpError),
#[error("Resource not found: {0}")]
NotFound(HttpError),
#[error("Resource conflict: {0}")]
Conflict(HttpError),
#[error("Rate limited: {error}")]
RateLimited {
error: HttpError,
retry_after: Option<Duration>,
},
#[error("HTTP transport error: {0}")]
Transport(Box<TransportError>),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Timestamp generation failed: {0}")]
Timestamp(#[from] SystemTimeError),
#[error("Signature generation failed")]
Signature,
#[error("Invalid configuration: {message}")]
InvalidConfig {
message: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
}
fn reqx_timeout_phase_name(error: &reqx::Error) -> Option<&'static str> {
match error {
reqx::Error::Timeout { phase, .. } => Some(match phase {
reqx::TimeoutPhase::Transport => "transport",
reqx::TimeoutPhase::ResponseBody => "response_body",
}),
_ => None,
}
}
fn reqx_transport_kind_name(error: &reqx::Error) -> Option<&'static str> {
match error {
reqx::Error::Transport { kind, .. } => Some(match kind {
reqx::TransportErrorKind::Dns => "dns",
reqx::TransportErrorKind::Connect => "connect",
reqx::TransportErrorKind::Tls => "tls",
reqx::TransportErrorKind::Read => "read",
reqx::TransportErrorKind::Other => "other",
}),
_ => None,
}
}
impl From<reqx::Error> for Error {
fn from(source: reqx::Error) -> Self {
let code = source.code().as_str();
let status = source.status_code();
let request_id = source.request_id().map(ToOwned::to_owned);
let retry_after = source.retry_after(SystemTime::now());
let method = source.request_method().map(ToString::to_string);
let uri = source.request_uri_redacted_owned();
let timeout_phase = reqx_timeout_phase_name(&source);
let transport_kind = reqx_transport_kind_name(&source);
let retryable = match source.code() {
reqx::ErrorCode::Timeout
| reqx::ErrorCode::DeadlineExceeded
| reqx::ErrorCode::Transport
| reqx::ErrorCode::RetryBudgetExhausted
| reqx::ErrorCode::CircuitOpen => true,
reqx::ErrorCode::HttpStatus => matches!(status, Some(429 | 500..=599)),
_ => false,
};
if let Some(status) = status {
let error = HttpError {
status,
message: None,
request_id: request_id.clone(),
body_snippet: None,
};
return match status {
401 | 403 => Self::Auth(error),
404 => Self::NotFound(error),
409 | 412 => Self::Conflict(error),
429 => Self::RateLimited { retry_after, error },
_ => Self::Transport(Box::new(TransportError {
status: Some(status),
message: None,
request_id,
body_snippet: None,
retry_after,
retryable,
code,
method,
uri,
timeout_phase,
transport_kind,
})),
};
}
Self::Transport(Box::new(TransportError {
status: None,
message: Some(source.to_string()),
request_id,
body_snippet: None,
retry_after,
retryable,
code,
method,
uri,
timeout_phase,
transport_kind,
}))
}
}
impl Error {
#[must_use]
pub fn kind(&self) -> ErrorKind {
match self {
Self::Auth(_) => ErrorKind::Auth,
Self::NotFound(_) => ErrorKind::NotFound,
Self::Conflict(_) => ErrorKind::Conflict,
Self::RateLimited { .. } => ErrorKind::RateLimited,
Self::Api { .. } => ErrorKind::Api,
Self::Transport(_) => ErrorKind::Transport,
Self::Serialization(_) => ErrorKind::Serialization,
Self::Timestamp(_) => ErrorKind::Timestamp,
Self::Signature => ErrorKind::Signature,
Self::InvalidConfig { .. } => ErrorKind::InvalidConfig,
}
}
#[must_use]
pub fn status(&self) -> Option<u16> {
match self {
Self::Auth(error) | Self::NotFound(error) | Self::Conflict(error) => Some(error.status),
Self::RateLimited { error, .. } => Some(error.status),
Self::Transport(error) => error.status,
_ => None,
}
}
#[must_use]
pub fn request_id(&self) -> Option<&str> {
match self {
Self::Api { request_id, .. } => request_id.as_deref(),
Self::Auth(error) | Self::NotFound(error) | Self::Conflict(error) => {
error.request_id.as_deref()
}
Self::RateLimited { error, .. } => error.request_id.as_deref(),
Self::Transport(error) => error.request_id.as_deref(),
_ => None,
}
}
#[must_use]
pub fn body_snippet(&self) -> Option<&str> {
match self {
Self::Api { body_snippet, .. } => body_snippet.as_deref(),
Self::Auth(error) | Self::NotFound(error) | Self::Conflict(error) => {
error.body_snippet.as_deref()
}
Self::RateLimited { error, .. } => error.body_snippet.as_deref(),
Self::Transport(error) => error.body_snippet.as_deref(),
_ => None,
}
}
#[must_use]
pub fn is_auth_error(&self) -> bool {
matches!(self, Self::Auth(_))
}
#[must_use]
pub fn is_retryable(&self) -> bool {
match self {
Self::RateLimited { .. } => true,
Self::Transport(error) => error.retryable,
Self::Api { code, .. } => matches!(*code, 130101 | 130102),
_ => false,
}
}
#[must_use]
pub fn retry_after(&self) -> Option<Duration> {
match self {
Self::RateLimited { retry_after, .. } => *retry_after,
Self::Transport(error) => error.retry_after,
_ => None,
}
}
}