use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
#[derive(Debug)]
pub enum AppError {
InvalidInput {
field: String,
reason: String,
},
InvalidUserId(String),
InvalidMemoryId(String),
InvalidEmbeddings(String),
ContentTooLarge {
size: usize,
max: usize,
},
ResourceLimit {
resource: String,
current: usize,
limit: usize,
},
AmbiguousMemoryId {
prefix: String,
count: usize,
},
MemoryNotFound(String),
UserNotFound(String),
TodoNotFound(String),
ProjectNotFound(String),
MemoryAlreadyExists(String),
StorageError(String),
DatabaseError(String),
SerializationError(String),
ConcurrencyError(String),
LockPoisoned {
resource: String,
details: String,
},
LockAcquisitionFailed {
resource: String,
reason: String,
},
ServiceUnavailable(String),
Internal(anyhow::Error),
}
impl AppError {
pub fn from_lock_poison<T>(resource: &str, _err: std::sync::PoisonError<T>) -> Self {
Self::LockPoisoned {
resource: resource.to_string(),
details: "Thread panicked while holding lock".to_string(),
}
}
pub fn lock_failed(resource: &str, reason: &str) -> Self {
Self::LockAcquisitionFailed {
resource: resource.to_string(),
reason: reason.to_string(),
}
}
pub fn code(&self) -> &'static str {
match self {
Self::InvalidInput { .. } => "INVALID_INPUT",
Self::InvalidUserId(_) => "INVALID_USER_ID",
Self::InvalidMemoryId(_) => "INVALID_MEMORY_ID",
Self::InvalidEmbeddings(_) => "INVALID_EMBEDDINGS",
Self::ContentTooLarge { .. } => "CONTENT_TOO_LARGE",
Self::AmbiguousMemoryId { .. } => "AMBIGUOUS_MEMORY_ID",
Self::ResourceLimit { .. } => "RESOURCE_LIMIT",
Self::MemoryNotFound(_) => "MEMORY_NOT_FOUND",
Self::UserNotFound(_) => "USER_NOT_FOUND",
Self::TodoNotFound(_) => "TODO_NOT_FOUND",
Self::ProjectNotFound(_) => "PROJECT_NOT_FOUND",
Self::MemoryAlreadyExists(_) => "MEMORY_ALREADY_EXISTS",
Self::StorageError(_) => "STORAGE_ERROR",
Self::DatabaseError(_) => "DATABASE_ERROR",
Self::SerializationError(_) => "SERIALIZATION_ERROR",
Self::ConcurrencyError(_) => "CONCURRENCY_ERROR",
Self::LockPoisoned { .. } => "LOCK_POISONED",
Self::LockAcquisitionFailed { .. } => "LOCK_ACQUISITION_FAILED",
Self::ServiceUnavailable(_) => "SERVICE_UNAVAILABLE",
Self::Internal(_) => "INTERNAL_ERROR",
}
}
pub fn status_code(&self) -> StatusCode {
match self {
Self::InvalidInput { .. }
| Self::InvalidUserId(_)
| Self::InvalidMemoryId(_)
| Self::InvalidEmbeddings(_)
| Self::ContentTooLarge { .. }
| Self::AmbiguousMemoryId { .. } => StatusCode::BAD_REQUEST,
Self::ResourceLimit { .. } => StatusCode::TOO_MANY_REQUESTS,
Self::MemoryNotFound(_)
| Self::UserNotFound(_)
| Self::TodoNotFound(_)
| Self::ProjectNotFound(_) => StatusCode::NOT_FOUND,
Self::MemoryAlreadyExists(_) => StatusCode::CONFLICT,
Self::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
Self::StorageError(_)
| Self::DatabaseError(_)
| Self::SerializationError(_)
| Self::ConcurrencyError(_)
| Self::LockPoisoned { .. }
| Self::LockAcquisitionFailed { .. }
| Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn message(&self) -> String {
match self {
Self::InvalidInput { field, reason } => {
format!("Invalid input for field '{field}': {reason}")
}
Self::InvalidUserId(msg) => format!("Invalid user ID: {msg}"),
Self::InvalidMemoryId(msg) => format!("Invalid memory ID: {msg}"),
Self::InvalidEmbeddings(msg) => format!("Invalid embeddings: {msg}"),
Self::ContentTooLarge { size, max } => {
format!("Content too large: {size} bytes (max: {max} bytes)")
}
Self::AmbiguousMemoryId { prefix, count } => {
format!("Ambiguous memory ID prefix '{prefix}': matches {count} memories. Use a longer prefix or full UUID.")
}
Self::ResourceLimit {
resource,
current,
limit,
} => {
format!("Resource limit exceeded for {resource}: current={current} MB, limit={limit} MB")
}
Self::MemoryNotFound(id) => format!("Memory not found: {id}"),
Self::UserNotFound(id) => format!("User not found: {id}"),
Self::TodoNotFound(id) => format!("Todo not found: {id}"),
Self::ProjectNotFound(id) => format!("Project not found: {id}"),
Self::MemoryAlreadyExists(id) => format!("Memory already exists: {id}"),
Self::StorageError(msg) => format!("Storage error: {msg}"),
Self::DatabaseError(msg) => format!("Database error: {msg}"),
Self::SerializationError(msg) => format!("Serialization error: {msg}"),
Self::ConcurrencyError(msg) => format!("Concurrency error: {msg}"),
Self::LockPoisoned { resource, details } => {
format!("Lock poisoned on resource '{resource}': {details}")
}
Self::LockAcquisitionFailed { resource, reason } => {
format!("Failed to acquire lock on '{resource}': {reason}")
}
Self::ServiceUnavailable(msg) => format!("Service unavailable: {msg}"),
Self::Internal(err) => format!("Internal error: {err}"),
}
}
pub fn to_response(&self) -> ErrorResponse {
ErrorResponse {
code: self.code().to_string(),
message: self.message(),
details: None,
request_id: None,
}
}
pub fn to_response_with_request_id(&self, request_id: Option<String>) -> ErrorResponse {
ErrorResponse {
code: self.code().to_string(),
message: self.message(),
details: None,
request_id,
}
}
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message())
}
}
impl std::error::Error for AppError {}
impl From<anyhow::Error> for AppError {
fn from(err: anyhow::Error) -> Self {
Self::Internal(err)
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = self.status_code();
let body = self.to_response();
(status, Json(body)).into_response()
}
}
pub trait ValidationErrorExt<T> {
fn map_validation_err(self, field: &str) -> Result<T>;
}
impl<T> ValidationErrorExt<T> for anyhow::Result<T> {
fn map_validation_err(self, field: &str) -> Result<T> {
self.map_err(|e| AppError::InvalidInput {
field: field.to_string(),
reason: e.to_string(),
})
}
}
pub type Result<T> = std::result::Result<T, AppError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_codes() {
assert_eq!(
AppError::InvalidUserId("test".to_string()).code(),
"INVALID_USER_ID"
);
assert_eq!(
AppError::MemoryNotFound("123".to_string()).code(),
"MEMORY_NOT_FOUND"
);
}
#[test]
fn test_status_codes() {
assert_eq!(
AppError::InvalidUserId("test".to_string()).status_code(),
StatusCode::BAD_REQUEST
);
assert_eq!(
AppError::MemoryNotFound("123".to_string()).status_code(),
StatusCode::NOT_FOUND
);
assert_eq!(
AppError::StorageError("failed".to_string()).status_code(),
StatusCode::INTERNAL_SERVER_ERROR
);
}
#[test]
fn test_error_response_serialization() {
let err = AppError::InvalidUserId("test123".to_string());
let response = err.to_response();
assert_eq!(response.code, "INVALID_USER_ID");
assert!(response.message.contains("test123"));
}
}