use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use export::data::{AppResponseData, NoData}; use thiserror::Error; pub type ProjectResult = Result; #[derive(Debug, Error)] pub enum ProjectError { #[error("bad request: {0}")] BadRequest(String), #[error("unauthorized: {0}")] Unauthorized(String), #[error("forbidden: {0}")] Forbidden(String), #[error("not found: {0}")] NotFound(String), #[error("conflict: {0}")] Conflict(String), #[error("unprocessable: {0}")] Unprocessable(String), #[error("rate limited: {0}")] RateLimited(String), #[error("internal: {0}")] Internal(String), #[error("database error: {0}")] Database(#[from] db::conn::SqlxError), #[error("uuid: {0}")] Uuid(#[from] uuid::Error), #[error("serde_json: {0}")] Json(#[from] serde_json::Error), #[error("cache: {0}")] Cache(String), #[error("space: {0}")] Space(String), } impl From for ProjectError { fn from(re: axum::extract::rejection::JsonRejection) -> Self { ProjectError::BadRequest(re.body_text()) } } impl From for ProjectError { fn from(re: axum::extract::rejection::PathRejection) -> Self { ProjectError::BadRequest(re.body_text()) } } impl From for ProjectError { fn from(re: axum::extract::rejection::QueryRejection) -> Self { ProjectError::BadRequest(re.body_text()) } } impl ProjectError { fn status(&self) -> StatusCode { match self { ProjectError::BadRequest(_) => StatusCode::BAD_REQUEST, ProjectError::Unauthorized(_) => StatusCode::UNAUTHORIZED, ProjectError::Forbidden(_) => StatusCode::FORBIDDEN, ProjectError::NotFound(_) => StatusCode::NOT_FOUND, ProjectError::Conflict(_) => StatusCode::CONFLICT, ProjectError::Unprocessable(_) => StatusCode::UNPROCESSABLE_ENTITY, ProjectError::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS, ProjectError::Internal(_) | ProjectError::Database(_) | ProjectError::Uuid(_) | ProjectError::Json(_) | ProjectError::Cache(_) | ProjectError::Space(_) => StatusCode::INTERNAL_SERVER_ERROR, } } fn is_user_facing(&self) -> bool { matches!( self, ProjectError::BadRequest(_) | ProjectError::Unauthorized(_) | ProjectError::Forbidden(_) | ProjectError::NotFound(_) | ProjectError::Conflict(_) | ProjectError::Unprocessable(_) | ProjectError::RateLimited(_) ) } /// Bounded label for metrics. Keep stable and low-cardinality. pub fn error_kind(&self) -> &'static str { match self { ProjectError::BadRequest(_) => "bad_request", ProjectError::Unauthorized(_) => "unauthorized", ProjectError::Forbidden(_) => "forbidden", ProjectError::NotFound(_) => "not_found", ProjectError::Conflict(_) => "conflict", ProjectError::Unprocessable(_) => "unprocessable", ProjectError::RateLimited(_) => "rate_limited", ProjectError::Internal(_) => "internal_other", ProjectError::Database(_) => "internal_db", ProjectError::Uuid(_) => "internal_uuid", ProjectError::Json(_) => "internal_json", ProjectError::Cache(_) => "internal_cache", ProjectError::Space(_) => "internal_space", } } } impl IntoResponse for ProjectError { fn into_response(self) -> Response { let status = self.status(); let msg = if self.is_user_facing() { self.to_string() } else { tracing::error!(error = %self, "project internal error"); "internal error".to_string() }; AppResponseData::::err_plain(status.as_u16() as i32, msg).into_response() } }