use anyhow::{Error as AnyhowError, anyhow};
use axum::{
extract::rejection::JsonRejection,
http::{StatusCode, header::CONTENT_TYPE},
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub enum RestError {
BadRequest(AnyhowError),
NotFound(AnyhowError),
UnprocessableEntity(AnyhowError),
TooManyRequests(AnyhowError),
ServiceUnavailable(AnyhowError),
InternalServerError(AnyhowError),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedRestError {
pub message: String,
pub error_type: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub chain: Vec<String>,
}
impl RestError {
pub fn bad_request(inner: anyhow::Error) -> Self {
Self::BadRequest(inner)
}
pub fn not_found(inner: anyhow::Error) -> Self {
Self::NotFound(inner)
}
pub fn unprocessable_entity(inner: anyhow::Error) -> Self {
Self::UnprocessableEntity(inner)
}
pub fn too_many_requests(inner: anyhow::Error) -> Self {
Self::TooManyRequests(inner)
}
pub fn service_unavailable(inner: anyhow::Error) -> Self {
Self::ServiceUnavailable(inner)
}
pub fn internal_server_error(inner: anyhow::Error) -> Self {
Self::InternalServerError(inner)
}
fn error_chain(error: &AnyhowError) -> Vec<String> {
let mut chain = vec![];
let mut source = error.source();
while let Some(err) = source {
chain.push(err.to_string());
source = err.source();
}
chain
}
}
impl IntoResponse for RestError {
fn into_response(self) -> Response {
let (status, error_type, error) = match self {
RestError::BadRequest(err) => (StatusCode::BAD_REQUEST, "bad_request", err),
RestError::NotFound(err) => (StatusCode::NOT_FOUND, "not_found", err),
RestError::UnprocessableEntity(err) => (StatusCode::UNPROCESSABLE_ENTITY, "unprocessable_entity", err),
RestError::TooManyRequests(err) => (StatusCode::TOO_MANY_REQUESTS, "too_many_requests", err),
RestError::ServiceUnavailable(err) => (StatusCode::SERVICE_UNAVAILABLE, "service_unavailable", err),
RestError::InternalServerError(err) => (StatusCode::INTERNAL_SERVER_ERROR, "internal_server_error", err),
};
let json_body = serde_json::to_string(&SerializedRestError {
message: error.to_string(),
error_type: error_type.to_string(),
chain: Self::error_chain(&error),
})
.unwrap_or_else(|err| format!("Failed to serialize error: {err}"));
info!("Returning REST error: {json_body}");
let mut response = Response::new(json_body.into());
*response.status_mut() = status;
response.headers_mut().insert(CONTENT_TYPE, "application/json".parse().unwrap());
response
}
}
impl From<anyhow::Error> for RestError {
fn from(err: anyhow::Error) -> Self {
Self::InternalServerError(err)
}
}
impl From<String> for RestError {
fn from(msg: String) -> Self {
Self::InternalServerError(anyhow::anyhow!(msg))
}
}
impl From<&str> for RestError {
fn from(msg: &str) -> Self {
Self::InternalServerError(anyhow::anyhow!(msg.to_string()))
}
}
impl From<JsonRejection> for RestError {
fn from(rejection: JsonRejection) -> Self {
match rejection {
JsonRejection::JsonDataError(err) => {
RestError::bad_request(anyhow!(err).context("Invalid JSON data in request body"))
}
JsonRejection::JsonSyntaxError(err) => {
RestError::bad_request(anyhow!(err).context("Invalid JSON syntax in request body"))
}
JsonRejection::MissingJsonContentType(_) => {
RestError::bad_request(anyhow!("Content-Type must be `application/json`"))
}
JsonRejection::BytesRejection(err) => {
RestError::bad_request(anyhow!(err).context("Failed to read request body"))
}
_ => RestError::bad_request(anyhow!("Invalid JSON request")),
}
}
}