use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use serde_json::json; use thiserror::Error; #[derive(Debug, Error)] pub enum AppError { #[error("not found: {0}")] NotFound(String), #[error("invalid request: {0}")] BadRequest(String), #[error("conflict: {0}")] Conflict(String), #[error("unauthorized")] Unauthorized, #[error("device communication failed: {0}")] Device(String), #[error("external dependency failed: {0}")] Dependency(String), #[error(transparent)] Internal(#[from] anyhow::Error), } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match &self { Self::NotFound(v) => (StatusCode::NOT_FOUND, v.clone()), Self::BadRequest(v) => (StatusCode::BAD_REQUEST, v.clone()), Self::Conflict(v) => (StatusCode::CONFLICT, v.clone()), Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()), Self::Device(v) => (StatusCode::BAD_GATEWAY, v.clone()), // Cloud/provider outages are an expected external-dependency failure, not a // controller-side 502. Keep Local's historical Device -> 502 mapping unchanged. Self::Dependency(v) => (StatusCode::FAILED_DEPENDENCY, v.clone()), Self::Internal(v) => { tracing::error!(error = ?v, "internal error"); ( StatusCode::INTERNAL_SERVER_ERROR, "internal server error".into(), ) } }; (status, Json(json!({"error": message}))).into_response() } } impl From for AppError { fn from(value: rusqlite::Error) -> Self { Self::Internal(value.into()) } } impl From for AppError { fn from(value: serde_json::Error) -> Self { Self::Internal(value.into()) } }