1use thiserror::Error;
2
3#[derive(Debug, Error)]
5#[non_exhaustive]
6pub enum CachekitError {
7 #[error("backend error: {0}")]
9 Backend(#[from] BackendError),
10
11 #[error("serialization error: {0}")]
13 Serialization(String),
14
15 #[error("encryption error: {0}")]
17 Encryption(String),
18
19 #[error("configuration error: {0}")]
21 Config(String),
22
23 #[error("payload too large: {size} bytes (limit: {limit} bytes)")]
25 PayloadTooLarge {
26 size: usize,
28 limit: usize,
30 },
31
32 #[error("invalid cache key: {0}")]
34 InvalidKey(String),
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum BackendErrorKind {
43 Transient,
45 Permanent,
47 Timeout,
49 Authentication,
51}
52
53impl BackendErrorKind {
54 #[must_use]
56 pub fn is_retryable(&self) -> bool {
57 matches!(self, Self::Transient | Self::Timeout)
58 }
59}
60
61impl std::fmt::Display for BackendErrorKind {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 Self::Transient => write!(f, "transient"),
65 Self::Permanent => write!(f, "permanent"),
66 Self::Timeout => write!(f, "timeout"),
67 Self::Authentication => write!(f, "authentication"),
68 }
69 }
70}
71
72#[derive(Debug, Error)]
76#[error("{kind} backend error: {message}")]
77pub struct BackendError {
78 pub kind: BackendErrorKind,
80 pub message: String,
82 #[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
84 #[source]
85 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
86 #[cfg(any(target_arch = "wasm32", feature = "unsync"))]
88 #[source]
89 pub source: Option<Box<dyn std::error::Error>>,
90}
91
92impl BackendError {
93 pub fn transient(message: impl Into<String>) -> Self {
95 Self {
96 kind: BackendErrorKind::Transient,
97 message: message.into(),
98 source: None,
99 }
100 }
101
102 pub fn permanent(message: impl Into<String>) -> Self {
104 Self {
105 kind: BackendErrorKind::Permanent,
106 message: message.into(),
107 source: None,
108 }
109 }
110
111 pub fn timeout(message: impl Into<String>) -> Self {
113 Self {
114 kind: BackendErrorKind::Timeout,
115 message: message.into(),
116 source: None,
117 }
118 }
119
120 pub fn auth(message: impl Into<String>) -> Self {
122 Self {
123 kind: BackendErrorKind::Authentication,
124 message: message.into(),
125 source: None,
126 }
127 }
128
129 pub fn sanitize_message(msg: &str, api_key: &str) -> String {
131 if api_key.is_empty() {
132 return msg.to_string();
133 }
134 msg.replace(api_key, "***")
135 }
136
137 pub fn from_http_status(status: u16, body: &[u8]) -> Self {
141 let body_str = std::str::from_utf8(body).unwrap_or("<non-utf8 body>");
142 let truncated: String = body_str.chars().take(256).collect();
143 let message = format!("HTTP {status}: {truncated}");
144
145 let kind = match status {
146 401 | 403 => BackendErrorKind::Authentication,
147 408 | 429 | 500 | 502 | 503 | 504 => BackendErrorKind::Transient,
148 _ if status >= 500 => BackendErrorKind::Transient,
149 _ => BackendErrorKind::Permanent,
150 };
151
152 Self {
153 kind,
154 message,
155 source: None,
156 }
157 }
158}