use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Serialize;
#[derive(Debug, Serialize, Clone)]
pub struct FieldError {
pub field: String,
pub code: String,
pub message: String,
}
#[derive(Debug, Serialize, Clone)]
pub struct ProblemDetails {
#[serde(rename = "type")]
pub kind: Cow<'static, str>,
pub title: Cow<'static, str>,
pub status: u16,
pub detail: Cow<'static, str>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub errors: Vec<FieldError>,
}
impl ProblemDetails {
#[inline]
pub fn new(
status: u16,
kind: &'static str,
title: &'static str,
detail: impl Into<Cow<'static, str>>,
) -> Self {
Self {
kind: Cow::Borrowed(kind),
title: Cow::Borrowed(title),
status,
detail: detail.into(),
errors: Vec::new(),
}
}
#[inline]
pub fn with_errors(mut self, errors: Vec<FieldError>) -> Self {
self.errors = errors;
self
}
}
pub trait HttpError: StdError + Send + Sync + 'static {
fn problem(&self) -> ProblemDetails;
}
pub struct HttpException(pub Box<dyn HttpError>);
impl fmt::Debug for HttpException {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "HttpException({})", self.0)
}
}
impl fmt::Display for HttpException {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl<E: HttpError + 'static> From<E> for HttpException {
#[inline]
fn from(e: E) -> Self {
Self(Box::new(e))
}
}
impl IntoResponse for HttpException {
fn into_response(self) -> Response {
let p = self.0.problem();
let status = StatusCode::from_u16(p.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(status, axum::Json(p)).into_response()
}
}
macro_rules! stock_error {
($name:ident, $status:expr, $kind:expr, $title:expr, $default_detail:expr) => {
#[derive(Debug, Clone)]
pub struct $name {
pub detail: Cow<'static, str>,
}
impl $name {
#[inline]
pub fn new(detail: impl Into<Cow<'static, str>>) -> Self {
Self {
detail: detail.into(),
}
}
}
impl Default for $name {
fn default() -> Self {
Self {
detail: Cow::Borrowed($default_detail),
}
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", $title, self.detail)
}
}
impl StdError for $name {}
impl HttpError for $name {
fn problem(&self) -> ProblemDetails {
ProblemDetails::new($status, $kind, $title, self.detail.clone())
}
}
};
}
stock_error!(
NotFound,
404,
"not-found",
"Not Found",
"resource not found"
);
stock_error!(
Unauthorized,
401,
"unauthorized",
"Unauthorized",
"authentication required"
);
stock_error!(Forbidden, 403, "forbidden", "Forbidden", "access denied");
stock_error!(
BadRequest,
400,
"bad-request",
"Bad Request",
"malformed request"
);
stock_error!(Conflict, 409, "conflict", "Conflict", "resource conflict");
stock_error!(
TooManyRequests,
429,
"too-many-requests",
"Too Many Requests",
"rate limit exceeded"
);
stock_error!(
ServiceUnavailable,
503,
"service-unavailable",
"Service Unavailable",
"downstream unavailable"
);
stock_error!(
Internal,
500,
"internal",
"Internal Server Error",
"internal error"
);
stock_error!(
GatewayTimeout,
504,
"gateway-timeout",
"Gateway Timeout",
"handler deadline exceeded"
);
#[derive(Debug, Clone)]
pub struct Validation {
pub errors: Vec<FieldError>,
}
impl Validation {
#[inline]
pub fn new(errors: Vec<FieldError>) -> Self {
Self { errors }
}
}
impl fmt::Display for Validation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "validation failed ({} error(s))", self.errors.len())
}
}
impl StdError for Validation {}
impl HttpError for Validation {
fn problem(&self) -> ProblemDetails {
ProblemDetails::new(
422,
"validation",
"Unprocessable Entity",
"payload failed validation",
)
.with_errors(self.errors.clone())
}
}
impl From<validator::ValidationErrors> for Validation {
fn from(errs: validator::ValidationErrors) -> Self {
let mut out = Vec::new();
for (field, kinds) in errs.field_errors() {
for k in kinds {
let message = k
.message
.as_ref()
.map(|m| m.to_string())
.unwrap_or_else(|| format!("failed `{}` rule", k.code));
out.push(FieldError {
field: field.to_string(),
code: k.code.to_string(),
message,
});
}
}
Self::new(out)
}
}
#[derive(Debug)]
pub enum Error {
NotFound,
Unauthorized,
Forbidden,
TooManyRequests,
ServiceUnavailable(&'static str),
BadRequest(&'static str),
Validation(Vec<FieldError>),
Internal(&'static str),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::NotFound => write!(f, "not found"),
Error::Unauthorized => write!(f, "unauthorized"),
Error::Forbidden => write!(f, "forbidden"),
Error::TooManyRequests => write!(f, "rate limit exceeded"),
Error::ServiceUnavailable(d) => write!(f, "service unavailable: {d}"),
Error::BadRequest(d) => write!(f, "bad request: {d}"),
Error::Validation(_) => write!(f, "validation failed"),
Error::Internal(d) => write!(f, "internal: {d}"),
}
}
}
impl StdError for Error {}
impl HttpError for Error {
fn problem(&self) -> ProblemDetails {
match self {
Error::NotFound => NotFound::default().problem(),
Error::Unauthorized => Unauthorized::default().problem(),
Error::Forbidden => Forbidden::default().problem(),
Error::TooManyRequests => TooManyRequests::default().problem(),
Error::ServiceUnavailable(d) => ServiceUnavailable::new(*d).problem(),
Error::BadRequest(d) => BadRequest::new(*d).problem(),
Error::Validation(v) => Validation::new(v.clone()).problem(),
Error::Internal(d) => Internal::new(*d).problem(),
}
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
HttpException::from(self).into_response()
}
}
impl From<validator::ValidationErrors> for Error {
fn from(errs: validator::ValidationErrors) -> Self {
Error::Validation(Validation::from(errs).errors)
}
}