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