113 lines
4 KiB
Rust
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())
|
|
}
|
|
}
|