shodh_memory/
errors.rs

1//! Enterprise-grade error handling with structured error types and codes
2//! Provides detailed error information for debugging and client error handling
3
4use axum::{
5    http::StatusCode,
6    response::{IntoResponse, Response},
7    Json,
8};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// Structured error response for API clients
13#[derive(Debug, Serialize, Deserialize)]
14pub struct ErrorResponse {
15    /// Machine-readable error code
16    pub code: String,
17
18    /// Human-readable error message
19    pub message: String,
20
21    /// Additional error context
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub details: Option<String>,
24
25    /// Request ID for tracing (enterprise feature)
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub request_id: Option<String>,
28}
29
30/// Application error types with proper categorization
31#[derive(Debug)]
32pub enum AppError {
33    // Validation Errors (400)
34    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    // Resource Limit Errors (429)
47    ResourceLimit {
48        resource: String,
49        current: usize,
50        limit: usize,
51    },
52
53    // Not Found Errors (404)
54    MemoryNotFound(String),
55    UserNotFound(String),
56    TodoNotFound(String),
57    ProjectNotFound(String),
58
59    // Conflict Errors (409)
60    MemoryAlreadyExists(String),
61
62    // Internal Errors (500)
63    StorageError(String),
64    DatabaseError(String),
65    SerializationError(String),
66    ConcurrencyError(String),
67
68    // Lock failures (500) - non-panicking lock handling
69    LockPoisoned {
70        resource: String,
71        details: String,
72    },
73    LockAcquisitionFailed {
74        resource: String,
75        reason: String,
76    },
77
78    // Service Errors (503)
79    ServiceUnavailable(String),
80
81    // Generic wrapper for external errors
82    Internal(anyhow::Error),
83}
84
85impl AppError {
86    /// Create a lock poisoned error from a PoisonError
87    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    /// Create a lock acquisition failure
95    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    /// Get error code for client identification
103    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    /// Get HTTP status code
128    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    /// Get detailed error message
158    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    /// Convert to structured error response
197    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    /// Convert to structured error response with request ID
207    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
225/// Convert from anyhow::Error to AppError
226impl From<anyhow::Error> for AppError {
227    fn from(err: anyhow::Error) -> Self {
228        Self::Internal(err)
229    }
230}
231
232/// Axum IntoResponse implementation for proper HTTP responses
233impl 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
242/// Helper trait to convert validation errors
243pub 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
256/// Type alias for Results using AppError
257pub 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}