//! Error types for the Slices API. //! //! All error types follow the pattern: `error-slices-{domain}-{number} {message}` //! This provides consistent error tracking and debugging across the application. //! //! ## Error Hierarchy //! //! ```text //! AppError (HTTP boundary layer) //! ├─> DatabaseError (data layer) //! ├─> SyncError (sync service) //! ├─> JetstreamError (real-time processing) //! ├─> ActorResolverError (identity resolution) //! └─> BlobUploadError (ATProto extensions) //! ``` use axum::{ Json, http::StatusCode, response::{IntoResponse, Response}, }; use thiserror::Error; // ============================================================================= // Database Layer Errors // ============================================================================= /// Database operation errors from the data access layer. #[derive(Error, Debug)] pub enum DatabaseError { #[error("error-slices-database-1 SQL query failed: {0}")] SqlQuery(#[from] sqlx::Error), #[error("error-slices-database-2 Record not found: {uri}")] RecordNotFound { uri: String }, } // ============================================================================= // Sync Service Errors // ============================================================================= /// Errors from background sync operations with ATProto relay. #[derive(Error, Debug)] pub enum SyncError { #[error("error-slices-sync-1 HTTP request failed: {0}")] HttpRequest(#[from] reqwest::Error), #[error("error-slices-sync-2 Database operation failed: {0}")] Database(#[from] DatabaseError), #[error("error-slices-sync-3 JSON parsing failed: {0}")] JsonParse(#[from] serde_json::Error), #[error("error-slices-sync-4 Failed to list repos for collection: {status}")] ListRepos { status: u16 }, #[error("error-slices-sync-5 Failed to list records: {status}")] ListRecords { status: u16 }, #[error("error-slices-sync-6 Task join failed: {0}")] TaskJoin(#[from] tokio::task::JoinError), #[error("error-slices-sync-7 Generic error: {0}")] Generic(String), #[error("error-slices-sync-8 Database query failed: {0}")] DatabaseQuery(String), #[error("error-slices-sync-9 Job cancelled by user")] Cancelled, } // ============================================================================= // Jetstream / Real-time Processing Errors // ============================================================================= /// Errors from Jetstream event stream processing. #[derive(Error, Debug)] pub enum JetstreamError { #[error("error-slices-jetstream-1 Connection failed: {message}")] ConnectionFailed { message: String }, #[error("error-slices-jetstream-2 Database error: {0}")] Database(#[from] DatabaseError), } // ============================================================================= // ATProto Identity Resolution Errors // ============================================================================= /// Errors from DID and handle resolution operations. #[derive(Error, Debug)] pub enum ActorResolverError { #[error("error-slice-actor-1 Failed to resolve DID: {0}")] ResolveFailed(String), #[error("error-slice-actor-2 Failed to parse DID: {0}")] ParseFailed(String), #[error("error-slice-actor-3 Subject resolved to handle instead of DID")] InvalidSubject, } // ============================================================================= // ATProto Extensions Errors // ============================================================================= /// Errors from ATProto blob upload operations. #[derive(Error, Debug)] pub enum BlobUploadError { #[error("error-slice-blob-1 HTTP request failed: {0}")] HttpRequest(#[from] reqwest_middleware::Error), #[error("error-slice-blob-2 JSON parsing failed: {0}")] JsonParse(#[from] serde_json::Error), #[error("error-slice-blob-3 DPoP proof creation failed: {0}")] DPoPProof(String), #[error("error-slice-blob-4 Upload request failed: {status} - {message}")] UploadFailed { status: u16, message: String }, } // ============================================================================= // Core Application Errors (HTTP Boundary Layer) // ============================================================================= /// Top-level application errors for HTTP handlers and server operations. /// /// This is the boundary layer that converts domain errors into HTTP responses. /// Domain-specific errors are wrapped and converted to appropriate HTTP status codes. #[derive(Error, Debug)] pub enum AppError { #[error("error-slices-app-1 Database error: {0}")] Database(#[from] DatabaseError), #[error("error-slices-app-2 Sync error: {0}")] Sync(#[from] SyncError), #[error("error-slices-app-3 Jetstream error: {0}")] Jetstream(#[from] JetstreamError), #[error("error-slices-app-4 Actor resolution error: {0}")] ActorResolver(#[from] ActorResolverError), #[error("error-slices-app-5 Blob upload error: {0}")] BlobUpload(#[from] BlobUploadError), #[error("error-slices-app-6 Database connection failed: {0}")] DatabaseConnection(#[from] sqlx::Error), #[error("error-slices-app-7 Database migration failed: {0}")] Migration(#[from] sqlx::migrate::MigrateError), #[error("error-slices-app-8 Server bind failed: {0}")] ServerBind(#[from] std::io::Error), #[error("error-slices-app-9 Cache error: {0}")] Cache(#[from] anyhow::Error), #[error("error-slices-app-10 Bad request: {0}")] BadRequest(String), #[error("error-slices-app-11 Resource not found: {0}")] NotFound(String), #[error("error-slices-app-12 Authentication required: {0}")] AuthRequired(String), #[error("error-slices-app-13 Forbidden: {0}")] Forbidden(String), #[error("error-slices-app-14 Internal server error: {0}")] Internal(String), } impl From for AppError { fn from(status: StatusCode) -> Self { match status { StatusCode::BAD_REQUEST => AppError::BadRequest("Bad request".to_string()), StatusCode::UNAUTHORIZED => { AppError::AuthRequired("Authentication required".to_string()) } StatusCode::FORBIDDEN => AppError::Forbidden("Forbidden".to_string()), StatusCode::NOT_FOUND => AppError::NotFound("Not found".to_string()), _ => AppError::Internal(format!("HTTP error: {}", status)), } } } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_name, error_message) = match &self { AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BadRequest", msg.clone()), AppError::NotFound(msg) => (StatusCode::NOT_FOUND, "NotFound", msg.clone()), AppError::AuthRequired(msg) => ( StatusCode::UNAUTHORIZED, "AuthenticationRequired", msg.clone(), ), AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, "Forbidden", msg.clone()), AppError::Internal(msg) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", msg.clone(), ), // Domain errors - all map to internal server error AppError::Database(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), AppError::Sync(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), AppError::Jetstream(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), AppError::ActorResolver(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), AppError::BlobUpload(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), // Infrastructure errors AppError::DatabaseConnection(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), AppError::Migration(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), AppError::ServerBind(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), AppError::Cache(e) => ( StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string(), ), }; let body = Json(serde_json::json!({ "error": error_name, "message": error_message })); (status, body).into_response() } }