use axum_core::response::IntoResponse; use http::StatusCode; use std::fmt; /// An error type that wraps an HTTP status code and optional message. #[derive(Debug, Clone, PartialEq)] pub struct HttpError { pub status: StatusCode, pub message: Option, } impl HttpError { pub fn new>(status: StatusCode, message: M) -> Self { HttpError { status, message: Some(message.into()), } } pub fn err(status: StatusCode, message: impl Into) -> Result { Err(HttpError::new(status, message)) } // --- 4xx Client Errors --- pub fn bad_request(message: impl Into) -> Result { Self::err(StatusCode::BAD_REQUEST, message) } pub fn unauthorized(message: impl Into) -> Result { Self::err(StatusCode::UNAUTHORIZED, message) } pub fn payment_required(message: impl Into) -> Result { Self::err(StatusCode::PAYMENT_REQUIRED, message) } pub fn forbidden(message: impl Into) -> Result { Self::err(StatusCode::FORBIDDEN, message) } pub fn not_found(message: impl Into) -> Result { Self::err(StatusCode::NOT_FOUND, message) } pub fn method_not_allowed(message: impl Into) -> Result { Self::err(StatusCode::METHOD_NOT_ALLOWED, message) } pub fn not_acceptable(message: impl Into) -> Result { Self::err(StatusCode::NOT_ACCEPTABLE, message) } pub fn proxy_auth_required(message: impl Into) -> Result { Self::err(StatusCode::PROXY_AUTHENTICATION_REQUIRED, message) } pub fn request_timeout(message: impl Into) -> Result { Self::err(StatusCode::REQUEST_TIMEOUT, message) } pub fn conflict(message: impl Into) -> Result { Self::err(StatusCode::CONFLICT, message) } pub fn gone(message: impl Into) -> Result { Self::err(StatusCode::GONE, message) } pub fn length_required(message: impl Into) -> Result { Self::err(StatusCode::LENGTH_REQUIRED, message) } pub fn precondition_failed(message: impl Into) -> Result { Self::err(StatusCode::PRECONDITION_FAILED, message) } pub fn payload_too_large(message: impl Into) -> Result { Self::err(StatusCode::PAYLOAD_TOO_LARGE, message) } pub fn uri_too_long(message: impl Into) -> Result { Self::err(StatusCode::URI_TOO_LONG, message) } pub fn unsupported_media_type(message: impl Into) -> Result { Self::err(StatusCode::UNSUPPORTED_MEDIA_TYPE, message) } pub fn im_a_teapot(message: impl Into) -> Result { Self::err(StatusCode::IM_A_TEAPOT, message) } pub fn too_many_requests(message: impl Into) -> Result { Self::err(StatusCode::TOO_MANY_REQUESTS, message) } // --- 5xx Server Errors --- pub fn internal_server_error(message: impl Into) -> Result { Self::err(StatusCode::INTERNAL_SERVER_ERROR, message) } pub fn not_implemented(message: impl Into) -> Result { Self::err(StatusCode::NOT_IMPLEMENTED, message) } pub fn bad_gateway(message: impl Into) -> Result { Self::err(StatusCode::BAD_GATEWAY, message) } pub fn service_unavailable(message: impl Into) -> Result { Self::err(StatusCode::SERVICE_UNAVAILABLE, message) } pub fn gateway_timeout(message: impl Into) -> Result { Self::err(StatusCode::GATEWAY_TIMEOUT, message) } pub fn http_version_not_supported(message: impl Into) -> Result { Self::err(StatusCode::HTTP_VERSION_NOT_SUPPORTED, message) } // --- 2xx/3xx (rare, but for completeness) --- pub fn ok(message: impl Into) -> Result { Self::err(StatusCode::OK, message) } pub fn created(message: impl Into) -> Result { Self::err(StatusCode::CREATED, message) } pub fn accepted(message: impl Into) -> Result { Self::err(StatusCode::ACCEPTED, message) } pub fn moved_permanently(message: impl Into) -> Result { Self::err(StatusCode::MOVED_PERMANENTLY, message) } pub fn found(message: impl Into) -> Result { Self::err(StatusCode::FOUND, message) } pub fn see_other(message: impl Into) -> Result { Self::err(StatusCode::SEE_OTHER, message) } pub fn not_modified(message: impl Into) -> Result { Self::err(StatusCode::NOT_MODIFIED, message) } } impl fmt::Display for HttpError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.message { Some(msg) => write!(f, "{}: {}", self.status, msg), None => write!(f, "{}", self.status), } } } impl std::error::Error for HttpError {} /// Trait to convert errors into HttpError with a given status code. pub trait OrHttpError: Sized { fn or_http_error(self, status: StatusCode, message: impl Into) -> Result; // --- Most common user-facing status codes --- fn or_bad_request(self, message: impl Into) -> Result { self.or_http_error(StatusCode::BAD_REQUEST, message) } fn or_unauthorized(self, message: impl Into) -> Result { self.or_http_error(StatusCode::UNAUTHORIZED, message) } fn or_forbidden(self, message: impl Into) -> Result { self.or_http_error(StatusCode::FORBIDDEN, message) } fn or_not_found(self, message: impl Into) -> Result { self.or_http_error(StatusCode::NOT_FOUND, message) } fn or_internal_server_error(self, message: impl Into) -> Result { self.or_http_error(StatusCode::INTERNAL_SERVER_ERROR, message) } } impl OrHttpError for Result where E: std::error::Error + Send + Sync + 'static, { fn or_http_error(self, status: StatusCode, message: impl Into) -> Result { self.map_err(|_| HttpError { status, message: Some(message.into()), }) } } impl OrHttpError for Option { fn or_http_error(self, status: StatusCode, message: impl Into) -> Result { self.ok_or_else(|| HttpError { status, message: Some(message.into()), }) } } impl OrHttpError<(), ()> for bool { fn or_http_error( self, status: StatusCode, message: impl Into, ) -> Result<(), HttpError> { if self { Ok(()) } else { Err(HttpError { status, message: Some(message.into()), }) } } } pub struct AnyhowMarker; impl OrHttpError for Result { fn or_http_error(self, status: StatusCode, message: impl Into) -> Result { self.map_err(|_| HttpError { status, message: Some(message.into()), }) } } impl IntoResponse for HttpError { fn into_response(self) -> axum_core::response::Response { let body = match &self.message { Some(msg) => msg.clone(), None => self .status .canonical_reason() .unwrap_or("Unknown error") .to_string(), }; (self.status, body).into_response() } }