use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CachekitError {
#[error("backend error: {0}")]
Backend(#[from] BackendError),
#[error("serialization error: {0}")]
Serialization(String),
#[error("encryption error: {0}")]
Encryption(String),
#[error("configuration error: {0}")]
Config(String),
#[error("payload too large: {size} bytes (limit: {limit} bytes)")]
PayloadTooLarge {
size: usize,
limit: usize,
},
#[error("invalid cache key: {0}")]
InvalidKey(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BackendErrorKind {
Transient,
Permanent,
Timeout,
Authentication,
}
impl BackendErrorKind {
#[must_use]
pub fn is_retryable(&self) -> bool {
matches!(self, Self::Transient | Self::Timeout)
}
}
impl std::fmt::Display for BackendErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Transient => write!(f, "transient"),
Self::Permanent => write!(f, "permanent"),
Self::Timeout => write!(f, "timeout"),
Self::Authentication => write!(f, "authentication"),
}
}
}
#[derive(Debug, Error)]
#[error("{kind} backend error: {message}")]
pub struct BackendError {
pub kind: BackendErrorKind,
pub message: String,
#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
#[source]
pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
#[source]
pub source: Option<Box<dyn std::error::Error>>,
}
impl BackendError {
pub fn transient(message: impl Into<String>) -> Self {
Self {
kind: BackendErrorKind::Transient,
message: message.into(),
source: None,
}
}
pub fn permanent(message: impl Into<String>) -> Self {
Self {
kind: BackendErrorKind::Permanent,
message: message.into(),
source: None,
}
}
pub fn timeout(message: impl Into<String>) -> Self {
Self {
kind: BackendErrorKind::Timeout,
message: message.into(),
source: None,
}
}
pub fn auth(message: impl Into<String>) -> Self {
Self {
kind: BackendErrorKind::Authentication,
message: message.into(),
source: None,
}
}
pub fn sanitize_message(msg: &str, api_key: &str) -> String {
if api_key.is_empty() {
return msg.to_string();
}
msg.replace(api_key, "***")
}
pub fn from_http_status(status: u16, body: &[u8]) -> Self {
let body_str = std::str::from_utf8(body).unwrap_or("<non-utf8 body>");
let truncated: String = body_str.chars().take(256).collect();
let message = format!("HTTP {status}: {truncated}");
let kind = match status {
401 | 403 => BackendErrorKind::Authentication,
408 | 429 | 500 | 502 | 503 | 504 => BackendErrorKind::Transient,
_ if status >= 500 => BackendErrorKind::Transient,
_ => BackendErrorKind::Permanent,
};
Self {
kind,
message,
source: None,
}
}
}