//! API error type mapping onto the status codes §4 specifies. use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use serde::Serialize; /// TRACES: DR-013 | SR-003 #[derive(Debug, thiserror::Error)] pub enum ApiError { /// §6 stage 2 — malformed, unrecognised or forbidden field. The message /// names the offending field so a client that forgets to strip `movie` or /// `jellyfin_id` gets a hard, diagnosable `400` (§6). #[error("{0}")] BadRequest(String), #[error("not found")] NotFound, /// §4 — identical `(identity, cut)` already exists from this contributor. #[error("{0}")] Conflict(String), #[error("{0}")] PayloadTooLarge(String), #[error("missing or invalid API token")] Unauthorized, /// §5 — carries the `Retry-After` value in seconds. #[error("rate limited")] RateLimited { retry_after: u64 }, #[error("internal error")] Internal(#[from] anyhow::Error), } #[derive(Serialize)] struct ErrorBody { error: String, message: String, } impl IntoResponse for ApiError { fn into_response(self) -> Response { let (status, code) = match &self { ApiError::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"), ApiError::NotFound => (StatusCode::NOT_FOUND, "not_found"), ApiError::Conflict(_) => (StatusCode::CONFLICT, "conflict"), ApiError::PayloadTooLarge(_) => (StatusCode::PAYLOAD_TOO_LARGE, "payload_too_large"), ApiError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"), ApiError::RateLimited { .. } => (StatusCode::TOO_MANY_REQUESTS, "rate_limited"), ApiError::Internal(e) => { // Internal detail is logged, never returned. tracing::error!(error = ?e, "internal error"); (StatusCode::INTERNAL_SERVER_ERROR, "internal") } }; let body = Json(ErrorBody { error: code.to_string(), message: match &self { ApiError::Internal(_) => "internal error".to_string(), other => other.to_string(), }, }); let mut resp = (status, body).into_response(); if let ApiError::RateLimited { retry_after } = self { if let Ok(v) = retry_after.to_string().parse() { resp.headers_mut().insert(axum::http::header::RETRY_AFTER, v); } } resp } } impl From for ApiError { fn from(e: rusqlite::Error) -> Self { ApiError::Internal(anyhow::Error::new(e)) } } pub type ApiResult = Result;