use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; #[derive(Debug)] pub enum AppError { BadRequest(String), Unauthorized(String), Forbidden(String), /// Uploads are temporarily locked (event closed / gallery released). Distinct from /// `Forbidden` so the client can tell this REVERSIBLE 403 apart from a permanent one /// (banned user, quota): the queued blob is kept and retried if the host reopens, /// instead of being purged like a genuinely-terminal rejection. UploadsLocked(String), NotFound(String), Conflict(String), /// Second field: optional retry-after seconds to include in the response. TooManyRequests(String, Option), /// Per-user storage quota exhausted. Distinct from `TooManyRequests` (rate limit) so /// the client can treat it as *terminal* (413, no retry) instead of backing off and /// retrying a permanently-failing upload forever. QuotaExceeded(String), Internal(anyhow::Error), } impl AppError { fn status_and_code(&self) -> (StatusCode, &str) { match self { Self::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"), Self::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "unauthorized"), Self::Forbidden(_) => (StatusCode::FORBIDDEN, "forbidden"), Self::UploadsLocked(_) => (StatusCode::FORBIDDEN, "uploads_locked"), Self::NotFound(_) => (StatusCode::NOT_FOUND, "not_found"), Self::Conflict(_) => (StatusCode::CONFLICT, "conflict"), Self::TooManyRequests(..) => (StatusCode::TOO_MANY_REQUESTS, "too_many_requests"), Self::QuotaExceeded(_) => (StatusCode::PAYLOAD_TOO_LARGE, "quota_exceeded"), Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal_error"), } } fn message(&self) -> String { match self { Self::BadRequest(msg) | Self::Unauthorized(msg) | Self::Forbidden(msg) | Self::UploadsLocked(msg) | Self::NotFound(msg) | Self::Conflict(msg) => msg.clone(), Self::TooManyRequests(msg, _) => msg.clone(), Self::QuotaExceeded(msg) => msg.clone(), Self::Internal(err) => { tracing::error!("internal error: {err:#}"); "Ein interner Fehler ist aufgetreten.".to_string() } } } } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, code) = self.status_and_code(); let retry_after_secs = if let Self::TooManyRequests(_, Some(secs)) = &self { Some(*secs) } else { None }; let message = self.message(); let mut body = serde_json::json!({ "error": code, "message": message, "status": status.as_u16(), }); if let Some(secs) = retry_after_secs { body["retry_after_secs"] = secs.into(); } let mut resp = (status, axum::Json(body)).into_response(); if let Some(secs) = retry_after_secs && let Ok(val) = axum::http::HeaderValue::from_str(&secs.to_string()) { resp.headers_mut() .insert(axum::http::header::RETRY_AFTER, val); } resp } } impl From for AppError { fn from(err: anyhow::Error) -> Self { Self::Internal(err) } } impl From for AppError { fn from(err: sqlx::Error) -> Self { Self::Internal(err.into()) } }