1use axum::{
5 http::StatusCode,
6 response::{IntoResponse, Response},
7 Json,
8};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Serialize, Deserialize)]
14pub struct ErrorResponse {
15 pub code: String,
17
18 pub message: String,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub details: Option<String>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub request_id: Option<String>,
28}
29
30#[derive(Debug)]
32pub enum AppError {
33 InvalidInput {
35 field: String,
36 reason: String,
37 },
38 InvalidUserId(String),
39 InvalidMemoryId(String),
40 InvalidEmbeddings(String),
41 ContentTooLarge {
42 size: usize,
43 max: usize,
44 },
45
46 ResourceLimit {
48 resource: String,
49 current: usize,
50 limit: usize,
51 },
52
53 AmbiguousMemoryId {
55 prefix: String,
56 count: usize,
57 },
58
59 MemoryNotFound(String),
61 UserNotFound(String),
62 TodoNotFound(String),
63 ProjectNotFound(String),
64
65 MemoryAlreadyExists(String),
67
68 StorageError(String),
70 DatabaseError(String),
71 SerializationError(String),
72 ConcurrencyError(String),
73
74 LockPoisoned {
76 resource: String,
77 details: String,
78 },
79 LockAcquisitionFailed {
80 resource: String,
81 reason: String,
82 },
83
84 ServiceUnavailable(String),
86
87 Internal(anyhow::Error),
89}
90
91impl AppError {
92 pub fn from_lock_poison<T>(resource: &str, _err: std::sync::PoisonError<T>) -> Self {
94 Self::LockPoisoned {
95 resource: resource.to_string(),
96 details: "Thread panicked while holding lock".to_string(),
97 }
98 }
99
100 pub fn lock_failed(resource: &str, reason: &str) -> Self {
102 Self::LockAcquisitionFailed {
103 resource: resource.to_string(),
104 reason: reason.to_string(),
105 }
106 }
107
108 pub fn code(&self) -> &'static str {
110 match self {
111 Self::InvalidInput { .. } => "INVALID_INPUT",
112 Self::InvalidUserId(_) => "INVALID_USER_ID",
113 Self::InvalidMemoryId(_) => "INVALID_MEMORY_ID",
114 Self::InvalidEmbeddings(_) => "INVALID_EMBEDDINGS",
115 Self::ContentTooLarge { .. } => "CONTENT_TOO_LARGE",
116 Self::AmbiguousMemoryId { .. } => "AMBIGUOUS_MEMORY_ID",
117 Self::ResourceLimit { .. } => "RESOURCE_LIMIT",
118 Self::MemoryNotFound(_) => "MEMORY_NOT_FOUND",
119 Self::UserNotFound(_) => "USER_NOT_FOUND",
120 Self::TodoNotFound(_) => "TODO_NOT_FOUND",
121 Self::ProjectNotFound(_) => "PROJECT_NOT_FOUND",
122 Self::MemoryAlreadyExists(_) => "MEMORY_ALREADY_EXISTS",
123 Self::StorageError(_) => "STORAGE_ERROR",
124 Self::DatabaseError(_) => "DATABASE_ERROR",
125 Self::SerializationError(_) => "SERIALIZATION_ERROR",
126 Self::ConcurrencyError(_) => "CONCURRENCY_ERROR",
127 Self::LockPoisoned { .. } => "LOCK_POISONED",
128 Self::LockAcquisitionFailed { .. } => "LOCK_ACQUISITION_FAILED",
129 Self::ServiceUnavailable(_) => "SERVICE_UNAVAILABLE",
130 Self::Internal(_) => "INTERNAL_ERROR",
131 }
132 }
133
134 pub fn status_code(&self) -> StatusCode {
136 match self {
137 Self::InvalidInput { .. }
138 | Self::InvalidUserId(_)
139 | Self::InvalidMemoryId(_)
140 | Self::InvalidEmbeddings(_)
141 | Self::ContentTooLarge { .. }
142 | Self::AmbiguousMemoryId { .. } => StatusCode::BAD_REQUEST,
143
144 Self::ResourceLimit { .. } => StatusCode::TOO_MANY_REQUESTS,
145
146 Self::MemoryNotFound(_)
147 | Self::UserNotFound(_)
148 | Self::TodoNotFound(_)
149 | Self::ProjectNotFound(_) => StatusCode::NOT_FOUND,
150
151 Self::MemoryAlreadyExists(_) => StatusCode::CONFLICT,
152
153 Self::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
154
155 Self::StorageError(_)
156 | Self::DatabaseError(_)
157 | Self::SerializationError(_)
158 | Self::ConcurrencyError(_)
159 | Self::LockPoisoned { .. }
160 | Self::LockAcquisitionFailed { .. }
161 | Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
162 }
163 }
164
165 pub fn message(&self) -> String {
167 match self {
168 Self::InvalidInput { field, reason } => {
169 format!("Invalid input for field '{field}': {reason}")
170 }
171 Self::InvalidUserId(msg) => format!("Invalid user ID: {msg}"),
172 Self::InvalidMemoryId(msg) => format!("Invalid memory ID: {msg}"),
173 Self::InvalidEmbeddings(msg) => format!("Invalid embeddings: {msg}"),
174 Self::ContentTooLarge { size, max } => {
175 format!("Content too large: {size} bytes (max: {max} bytes)")
176 }
177 Self::AmbiguousMemoryId { prefix, count } => {
178 format!("Ambiguous memory ID prefix '{prefix}': matches {count} memories. Use a longer prefix or full UUID.")
179 }
180 Self::ResourceLimit {
181 resource,
182 current,
183 limit,
184 } => {
185 format!("Resource limit exceeded for {resource}: current={current} MB, limit={limit} MB")
186 }
187 Self::MemoryNotFound(id) => format!("Memory not found: {id}"),
188 Self::UserNotFound(id) => format!("User not found: {id}"),
189 Self::TodoNotFound(id) => format!("Todo not found: {id}"),
190 Self::ProjectNotFound(id) => format!("Project not found: {id}"),
191 Self::MemoryAlreadyExists(id) => format!("Memory already exists: {id}"),
192 Self::StorageError(msg) => format!("Storage error: {msg}"),
193 Self::DatabaseError(msg) => format!("Database error: {msg}"),
194 Self::SerializationError(msg) => format!("Serialization error: {msg}"),
195 Self::ConcurrencyError(msg) => format!("Concurrency error: {msg}"),
196 Self::LockPoisoned { resource, details } => {
197 format!("Lock poisoned on resource '{resource}': {details}")
198 }
199 Self::LockAcquisitionFailed { resource, reason } => {
200 format!("Failed to acquire lock on '{resource}': {reason}")
201 }
202 Self::ServiceUnavailable(msg) => format!("Service unavailable: {msg}"),
203 Self::Internal(err) => format!("Internal error: {err}"),
204 }
205 }
206
207 pub fn to_response(&self) -> ErrorResponse {
209 ErrorResponse {
210 code: self.code().to_string(),
211 message: self.message(),
212 details: None,
213 request_id: None,
214 }
215 }
216
217 pub fn to_response_with_request_id(&self, request_id: Option<String>) -> ErrorResponse {
219 ErrorResponse {
220 code: self.code().to_string(),
221 message: self.message(),
222 details: None,
223 request_id,
224 }
225 }
226}
227
228impl fmt::Display for AppError {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 write!(f, "{}", self.message())
231 }
232}
233
234impl std::error::Error for AppError {}
235
236impl From<anyhow::Error> for AppError {
238 fn from(err: anyhow::Error) -> Self {
239 Self::Internal(err)
240 }
241}
242
243impl IntoResponse for AppError {
245 fn into_response(self) -> Response {
246 let status = self.status_code();
247 let body = self.to_response();
248
249 (status, Json(body)).into_response()
250 }
251}
252
253pub trait ValidationErrorExt<T> {
255 fn map_validation_err(self, field: &str) -> Result<T>;
256}
257
258impl<T> ValidationErrorExt<T> for anyhow::Result<T> {
259 fn map_validation_err(self, field: &str) -> Result<T> {
260 self.map_err(|e| AppError::InvalidInput {
261 field: field.to_string(),
262 reason: e.to_string(),
263 })
264 }
265}
266
267pub type Result<T> = std::result::Result<T, AppError>;
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_error_codes() {
276 assert_eq!(
277 AppError::InvalidUserId("test".to_string()).code(),
278 "INVALID_USER_ID"
279 );
280 assert_eq!(
281 AppError::MemoryNotFound("123".to_string()).code(),
282 "MEMORY_NOT_FOUND"
283 );
284 }
285
286 #[test]
287 fn test_status_codes() {
288 assert_eq!(
289 AppError::InvalidUserId("test".to_string()).status_code(),
290 StatusCode::BAD_REQUEST
291 );
292 assert_eq!(
293 AppError::MemoryNotFound("123".to_string()).status_code(),
294 StatusCode::NOT_FOUND
295 );
296 assert_eq!(
297 AppError::StorageError("failed".to_string()).status_code(),
298 StatusCode::INTERNAL_SERVER_ERROR
299 );
300 }
301
302 #[test]
303 fn test_error_response_serialization() {
304 let err = AppError::InvalidUserId("test123".to_string());
305 let response = err.to_response();
306
307 assert_eq!(response.code, "INVALID_USER_ID");
308 assert!(response.message.contains("test123"));
309 }
310}