Skip to main content

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    // Ambiguity Errors (400)
54    AmbiguousMemoryId {
55        prefix: String,
56        count: usize,
57    },
58
59    // Not Found Errors (404)
60    MemoryNotFound(String),
61    UserNotFound(String),
62    TodoNotFound(String),
63    ProjectNotFound(String),
64
65    // Conflict Errors (409)
66    MemoryAlreadyExists(String),
67
68    // Internal Errors (500)
69    StorageError(String),
70    DatabaseError(String),
71    SerializationError(String),
72    ConcurrencyError(String),
73
74    // Lock failures (500) - non-panicking lock handling
75    LockPoisoned {
76        resource: String,
77        details: String,
78    },
79    LockAcquisitionFailed {
80        resource: String,
81        reason: String,
82    },
83
84    // Service Errors (503)
85    ServiceUnavailable(String),
86
87    // Generic wrapper for external errors
88    Internal(anyhow::Error),
89}
90
91impl AppError {
92    /// Create a lock poisoned error from a PoisonError
93    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    /// Create a lock acquisition failure
101    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    /// Get error code for client identification
109    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    /// Get HTTP status code
135    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    /// Get detailed error message
166    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    /// Convert to structured error response
208    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    /// Convert to structured error response with request ID
218    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
236/// Convert from anyhow::Error to AppError
237impl From<anyhow::Error> for AppError {
238    fn from(err: anyhow::Error) -> Self {
239        Self::Internal(err)
240    }
241}
242
243/// Axum IntoResponse implementation for proper HTTP responses
244impl 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
253/// Helper trait to convert validation errors
254pub 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
267/// Type alias for Results using AppError
268pub 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}