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 MemoryNotFound(String),
55 UserNotFound(String),
56 TodoNotFound(String),
57 ProjectNotFound(String),
58
59 MemoryAlreadyExists(String),
61
62 StorageError(String),
64 DatabaseError(String),
65 SerializationError(String),
66 ConcurrencyError(String),
67
68 LockPoisoned {
70 resource: String,
71 details: String,
72 },
73 LockAcquisitionFailed {
74 resource: String,
75 reason: String,
76 },
77
78 ServiceUnavailable(String),
80
81 Internal(anyhow::Error),
83}
84
85impl AppError {
86 pub fn from_lock_poison<T>(resource: &str, _err: std::sync::PoisonError<T>) -> Self {
88 Self::LockPoisoned {
89 resource: resource.to_string(),
90 details: "Thread panicked while holding lock".to_string(),
91 }
92 }
93
94 pub fn lock_failed(resource: &str, reason: &str) -> Self {
96 Self::LockAcquisitionFailed {
97 resource: resource.to_string(),
98 reason: reason.to_string(),
99 }
100 }
101
102 pub fn code(&self) -> &'static str {
104 match self {
105 Self::InvalidInput { .. } => "INVALID_INPUT",
106 Self::InvalidUserId(_) => "INVALID_USER_ID",
107 Self::InvalidMemoryId(_) => "INVALID_MEMORY_ID",
108 Self::InvalidEmbeddings(_) => "INVALID_EMBEDDINGS",
109 Self::ContentTooLarge { .. } => "CONTENT_TOO_LARGE",
110 Self::ResourceLimit { .. } => "RESOURCE_LIMIT",
111 Self::MemoryNotFound(_) => "MEMORY_NOT_FOUND",
112 Self::UserNotFound(_) => "USER_NOT_FOUND",
113 Self::TodoNotFound(_) => "TODO_NOT_FOUND",
114 Self::ProjectNotFound(_) => "PROJECT_NOT_FOUND",
115 Self::MemoryAlreadyExists(_) => "MEMORY_ALREADY_EXISTS",
116 Self::StorageError(_) => "STORAGE_ERROR",
117 Self::DatabaseError(_) => "DATABASE_ERROR",
118 Self::SerializationError(_) => "SERIALIZATION_ERROR",
119 Self::ConcurrencyError(_) => "CONCURRENCY_ERROR",
120 Self::LockPoisoned { .. } => "LOCK_POISONED",
121 Self::LockAcquisitionFailed { .. } => "LOCK_ACQUISITION_FAILED",
122 Self::ServiceUnavailable(_) => "SERVICE_UNAVAILABLE",
123 Self::Internal(_) => "INTERNAL_ERROR",
124 }
125 }
126
127 pub fn status_code(&self) -> StatusCode {
129 match self {
130 Self::InvalidInput { .. }
131 | Self::InvalidUserId(_)
132 | Self::InvalidMemoryId(_)
133 | Self::InvalidEmbeddings(_)
134 | Self::ContentTooLarge { .. } => StatusCode::BAD_REQUEST,
135
136 Self::ResourceLimit { .. } => StatusCode::TOO_MANY_REQUESTS,
137
138 Self::MemoryNotFound(_)
139 | Self::UserNotFound(_)
140 | Self::TodoNotFound(_)
141 | Self::ProjectNotFound(_) => StatusCode::NOT_FOUND,
142
143 Self::MemoryAlreadyExists(_) => StatusCode::CONFLICT,
144
145 Self::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
146
147 Self::StorageError(_)
148 | Self::DatabaseError(_)
149 | Self::SerializationError(_)
150 | Self::ConcurrencyError(_)
151 | Self::LockPoisoned { .. }
152 | Self::LockAcquisitionFailed { .. }
153 | Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
154 }
155 }
156
157 pub fn message(&self) -> String {
159 match self {
160 Self::InvalidInput { field, reason } => {
161 format!("Invalid input for field '{field}': {reason}")
162 }
163 Self::InvalidUserId(msg) => format!("Invalid user ID: {msg}"),
164 Self::InvalidMemoryId(msg) => format!("Invalid memory ID: {msg}"),
165 Self::InvalidEmbeddings(msg) => format!("Invalid embeddings: {msg}"),
166 Self::ContentTooLarge { size, max } => {
167 format!("Content too large: {size} bytes (max: {max} bytes)")
168 }
169 Self::ResourceLimit {
170 resource,
171 current,
172 limit,
173 } => {
174 format!("Resource limit exceeded for {resource}: current={current} MB, limit={limit} MB")
175 }
176 Self::MemoryNotFound(id) => format!("Memory not found: {id}"),
177 Self::UserNotFound(id) => format!("User not found: {id}"),
178 Self::TodoNotFound(id) => format!("Todo not found: {id}"),
179 Self::ProjectNotFound(id) => format!("Project not found: {id}"),
180 Self::MemoryAlreadyExists(id) => format!("Memory already exists: {id}"),
181 Self::StorageError(msg) => format!("Storage error: {msg}"),
182 Self::DatabaseError(msg) => format!("Database error: {msg}"),
183 Self::SerializationError(msg) => format!("Serialization error: {msg}"),
184 Self::ConcurrencyError(msg) => format!("Concurrency error: {msg}"),
185 Self::LockPoisoned { resource, details } => {
186 format!("Lock poisoned on resource '{resource}': {details}")
187 }
188 Self::LockAcquisitionFailed { resource, reason } => {
189 format!("Failed to acquire lock on '{resource}': {reason}")
190 }
191 Self::ServiceUnavailable(msg) => format!("Service unavailable: {msg}"),
192 Self::Internal(err) => format!("Internal error: {err}"),
193 }
194 }
195
196 pub fn to_response(&self) -> ErrorResponse {
198 ErrorResponse {
199 code: self.code().to_string(),
200 message: self.message(),
201 details: None,
202 request_id: None,
203 }
204 }
205
206 pub fn to_response_with_request_id(&self, request_id: Option<String>) -> ErrorResponse {
208 ErrorResponse {
209 code: self.code().to_string(),
210 message: self.message(),
211 details: None,
212 request_id,
213 }
214 }
215}
216
217impl fmt::Display for AppError {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 write!(f, "{}", self.message())
220 }
221}
222
223impl std::error::Error for AppError {}
224
225impl From<anyhow::Error> for AppError {
227 fn from(err: anyhow::Error) -> Self {
228 Self::Internal(err)
229 }
230}
231
232impl IntoResponse for AppError {
234 fn into_response(self) -> Response {
235 let status = self.status_code();
236 let body = self.to_response();
237
238 (status, Json(body)).into_response()
239 }
240}
241
242pub trait ValidationErrorExt<T> {
244 fn map_validation_err(self, field: &str) -> Result<T>;
245}
246
247impl<T> ValidationErrorExt<T> for anyhow::Result<T> {
248 fn map_validation_err(self, field: &str) -> Result<T> {
249 self.map_err(|e| AppError::InvalidInput {
250 field: field.to_string(),
251 reason: e.to_string(),
252 })
253 }
254}
255
256pub type Result<T> = std::result::Result<T, AppError>;
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_error_codes() {
265 assert_eq!(
266 AppError::InvalidUserId("test".to_string()).code(),
267 "INVALID_USER_ID"
268 );
269 assert_eq!(
270 AppError::MemoryNotFound("123".to_string()).code(),
271 "MEMORY_NOT_FOUND"
272 );
273 }
274
275 #[test]
276 fn test_status_codes() {
277 assert_eq!(
278 AppError::InvalidUserId("test".to_string()).status_code(),
279 StatusCode::BAD_REQUEST
280 );
281 assert_eq!(
282 AppError::MemoryNotFound("123".to_string()).status_code(),
283 StatusCode::NOT_FOUND
284 );
285 assert_eq!(
286 AppError::StorageError("failed".to_string()).status_code(),
287 StatusCode::INTERNAL_SERVER_ERROR
288 );
289 }
290
291 #[test]
292 fn test_error_response_serialization() {
293 let err = AppError::InvalidUserId("test123".to_string());
294 let response = err.to_response();
295
296 assert_eq!(response.code, "INVALID_USER_ID");
297 assert!(response.message.contains("test123"));
298 }
299}