maps/backend/src/errors.rs
2026-03-30 09:22:16 +02:00

113 lines
4 KiB
Rust

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<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
tracing::error!(error = %err, "Database error");
AppError::ServiceUnavailable("Database is unreachable".into())
}
}
impl From<reqwest::Error> 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<redis::RedisError> 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())
}
}