use actix_web::{HttpResponse, ResponseError}; use serde::Serialize; use std::fmt; /// Standard JSON error body matching the API contract. #[derive(Debug, Serialize)] pub struct ErrorBody { pub error: ErrorDetail, } #[derive(Debug, Serialize)] pub struct ErrorDetail { pub code: String, pub message: String, } /// Application-level error type that maps to HTTP status codes and /// the standard error JSON format defined in API.md. #[derive(Debug)] pub enum AppError { MissingParameter(String), InvalidParameter(String), InvalidBbox(String), InvalidCoordinates(String), NotFound(String), RateLimited, UpstreamError(String), ServiceUnavailable(String), InternalError(String), } impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AppError::MissingParameter(msg) => write!(f, "Missing parameter: {msg}"), AppError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"), AppError::InvalidBbox(msg) => write!(f, "Invalid bbox: {msg}"), AppError::InvalidCoordinates(msg) => write!(f, "Invalid coordinates: {msg}"), AppError::NotFound(msg) => write!(f, "Not found: {msg}"), AppError::RateLimited => write!(f, "Rate limited"), AppError::UpstreamError(msg) => write!(f, "Upstream error: {msg}"), AppError::ServiceUnavailable(msg) => write!(f, "Service unavailable: {msg}"), AppError::InternalError(msg) => write!(f, "Internal error: {msg}"), } } } impl ResponseError for AppError { fn error_response(&self) -> HttpResponse { let (status, code) = match self { AppError::MissingParameter(_) => { (actix_web::http::StatusCode::BAD_REQUEST, "MISSING_PARAMETER") } AppError::InvalidParameter(_) => { (actix_web::http::StatusCode::BAD_REQUEST, "INVALID_PARAMETER") } AppError::InvalidBbox(_) => { (actix_web::http::StatusCode::BAD_REQUEST, "INVALID_BBOX") } AppError::InvalidCoordinates(_) => { (actix_web::http::StatusCode::BAD_REQUEST, "INVALID_COORDINATES") } AppError::NotFound(_) => (actix_web::http::StatusCode::NOT_FOUND, "NOT_FOUND"), AppError::RateLimited => { (actix_web::http::StatusCode::TOO_MANY_REQUESTS, "RATE_LIMITED") } AppError::UpstreamError(_) => { (actix_web::http::StatusCode::BAD_GATEWAY, "UPSTREAM_ERROR") } AppError::ServiceUnavailable(_) => ( actix_web::http::StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", ), AppError::InternalError(_) => ( actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", ), }; HttpResponse::build(status).json(ErrorBody { error: ErrorDetail { code: code.to_string(), message: self.to_string(), }, }) } } impl From for AppError { fn from(err: sqlx::Error) -> Self { tracing::error!(error = %err, "Database error"); AppError::ServiceUnavailable("Database is unreachable".into()) } } impl From for AppError { fn from(err: reqwest::Error) -> Self { tracing::error!(error = %err, "HTTP client error"); if err.is_connect() || err.is_timeout() { AppError::ServiceUnavailable("Upstream service is unreachable".into()) } else { AppError::UpstreamError("Upstream service returned an error".into()) } } } impl From for AppError { fn from(err: redis::RedisError) -> Self { tracing::warn!(error = %err, "Redis error (non-fatal)"); // Redis errors are non-fatal — we skip cache and proceed AppError::InternalError("Cache error".into()) } }