use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde_json::json; pub enum AppError { Unauthorized, BadRequest(String), NotFound(String), /// 403 — authenticated but lacks permission (e.g. non-admin calling an /// admin-only endpoint). Forbidden(String), /// 409 — request is valid but conflicts with current state (e.g. an /// analysis is already running for this case). Conflict(String), /// 503 — the feature is unavailable right now (e.g. no LLM configured). ServiceUnavailable(String), Internal(String), /// 302 redirect. Used by web auth to send unauthenticated users to login. Redirect(String), } impl IntoResponse for AppError { fn into_response(self) -> Response { if let Self::Redirect(url) = &self { return Response::builder() .status(StatusCode::FOUND) .header(axum::http::header::LOCATION, url) .body(axum::body::Body::empty()) .unwrap(); } let (status, message) = match &self { Self::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()), Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), Self::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), Self::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()), Self::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()), Self::ServiceUnavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg.clone()), Self::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()), Self::Redirect(_) => unreachable!(), }; let body = axum::Json(json!({ "error": message })); (status, body).into_response() } } impl From for AppError { fn from(err: std::io::Error) -> Self { Self::Internal(err.to_string()) } }