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
57    // Conflict Errors (409)
58    MemoryAlreadyExists(String),
59
60    // Internal Errors (500)
61    StorageError(String),
62    DatabaseError(String),
63    SerializationError(String),
64    ConcurrencyError(String),
65
66    // Lock failures (500) - non-panicking lock handling
67    LockPoisoned {
68        resource: String,
69        details: String,
70    },
71    LockAcquisitionFailed {
72        resource: String,
73        reason: String,
74    },
75
76    // Service Errors (503)
77    ServiceUnavailable(String),
78
79    // Generic wrapper for external errors
80    Internal(anyhow::Error),
81}
82
83impl AppError {
84    /// Create a lock poisoned error from a PoisonError
85    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    /// Create a lock acquisition failure
93    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    /// Get error code for client identification
101    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    /// Get HTTP status code
124    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    /// Get detailed error message
151    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    /// Convert to structured error response
188    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    /// Convert to structured error response with request ID
198    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
216/// Convert from anyhow::Error to AppError
217impl From<anyhow::Error> for AppError {
218    fn from(err: anyhow::Error) -> Self {
219        Self::Internal(err)
220    }
221}
222
223/// Axum IntoResponse implementation for proper HTTP responses
224impl 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
233/// Helper trait to convert validation errors
234pub 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
247/// Type alias for Results using AppError
248pub 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}