use anyhow::Error as AnyhowError;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::response::Response;
use axum::Json;
use serde::Deserialize;
use serde::Serialize;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use super::RouteErrorOutput;
use crate::RouteInternalErrorOutput;
pub struct RouteError<S = (), const EXPOSE_INTERNAL_ERROR: bool = false>
where
S: Serialize + for<'a> Deserialize<'a> + Debug,
{
status_code: StatusCode,
error: Option<AnyhowError>,
extra_data: Option<Box<S>>,
public_error_message: Option<String>,
}
impl RouteError<()> {
pub fn new_unauthorized() -> RouteError<()> {
Self::new_from_status(StatusCode::UNAUTHORIZED)
}
pub fn new_not_found() -> RouteError<()> {
Self::new_from_status(StatusCode::NOT_FOUND)
}
pub fn new_bad_request() -> RouteError<()> {
Self::new_from_status(StatusCode::BAD_REQUEST)
}
pub fn new_internal_server() -> RouteError<()> {
Self::new_from_status(StatusCode::INTERNAL_SERVER_ERROR)
}
pub fn new_conflict() -> RouteError<()> {
Self::new_from_status(StatusCode::CONFLICT)
}
pub fn new_forbidden() -> RouteError<()> {
Self::new_from_status(StatusCode::FORBIDDEN)
}
pub fn new_from_status(status_code: StatusCode) -> RouteError<()> {
Self {
status_code,
..Self::default()
}
}
}
impl<S, const EXPOSE_INTERNAL_ERROR: bool> RouteError<S, EXPOSE_INTERNAL_ERROR>
where
S: Serialize + for<'a> Deserialize<'a> + Debug,
{
pub fn set_status_code(self, status_code: StatusCode) -> Self {
Self {
status_code,
..self
}
}
pub fn set_error(self, error: AnyhowError) -> Self {
Self {
error: Some(error),
..self
}
}
pub fn set_error_data<NewS>(self, extra_data: NewS) -> RouteError<NewS>
where
NewS: Serialize + for<'a> Deserialize<'a> + Debug,
{
RouteError {
extra_data: Some(Box::new(extra_data)),
status_code: self.status_code,
error: self.error,
public_error_message: self.public_error_message,
}
}
pub fn set_public_error_message(self, public_error_message: &str) -> Self {
Self {
public_error_message: Some(public_error_message.to_string()),
..self
}
}
pub fn public_error_message<'a>(&'a self) -> &'a str {
if let Some(public_error_message) = self.public_error_message.as_ref() {
return public_error_message;
}
status_code_to_public_message(self.status_code())
}
pub fn status_code(&self) -> StatusCode {
self.status_code
}
}
impl<S, const EXPOSE_INTERNAL_ERROR: bool> Default for RouteError<S, EXPOSE_INTERNAL_ERROR>
where
S: Serialize + for<'a> Deserialize<'a> + Debug,
{
fn default() -> Self {
Self {
status_code: StatusCode::INTERNAL_SERVER_ERROR,
error: None,
extra_data: None,
public_error_message: None,
}
}
}
impl<S, const EXPOSE_INTERNAL_ERROR: bool> IntoResponse for RouteError<S, EXPOSE_INTERNAL_ERROR>
where
S: Serialize + for<'a> Deserialize<'a> + Debug,
{
fn into_response(self) -> Response {
let status = self.status_code();
let extra_data = self.extra_data;
let error = match self.public_error_message {
Some(public_error_message) => public_error_message,
None => status_code_to_public_message(status).to_string(),
};
let internal_error = if EXPOSE_INTERNAL_ERROR {
self.error.map(|err| RouteInternalErrorOutput {
name: format!("{}", err),
debug: format!("{:?}", err),
})
} else {
None
};
let output = RouteErrorOutput {
error,
internal_error,
extra_data,
..RouteErrorOutput::default()
};
let body = Json(output);
(status, body).into_response()
}
}
impl<S, const EXPOSE_INTERNAL_ERROR: bool> Debug for RouteError<S, EXPOSE_INTERNAL_ERROR>
where
S: Serialize + for<'a> Deserialize<'a> + Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}, {:?}", self.public_error_message(), self.error)
}
}
impl<S, const EXPOSE_INTERNAL_ERROR: bool> Display for RouteError<S, EXPOSE_INTERNAL_ERROR>
where
S: Serialize + for<'a> Deserialize<'a> + Debug,
{
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{}", self.public_error_message())
}
}
impl<S, const EXPOSE_INTERNAL_ERROR: bool, FE> From<FE> for RouteError<S, EXPOSE_INTERNAL_ERROR>
where
S: Serialize + for<'a> Deserialize<'a> + Debug,
FE: Into<AnyhowError>,
{
fn from(error: FE) -> Self {
let anyhow_error: AnyhowError = error.into();
::tracing::error!("{:?}", anyhow_error);
RouteError {
status_code: StatusCode::INTERNAL_SERVER_ERROR,
error: Some(anyhow_error),
..Self::default()
}
}
}
fn status_code_to_public_message(status_code: StatusCode) -> &'static str {
match status_code {
StatusCode::CONFLICT => "The request is not allowed",
StatusCode::UNAUTHORIZED => "You are not authorised to access this endpoint",
StatusCode::NOT_FOUND => "The resource was not found",
StatusCode::BAD_REQUEST => "Bad request made",
StatusCode::FORBIDDEN => "Request is forbidden",
StatusCode::IM_A_TEAPOT => "I'm a teapot",
StatusCode::TOO_MANY_REQUESTS => "Too many requests",
StatusCode::BAD_GATEWAY => "Bad gateway",
StatusCode::SERVICE_UNAVAILABLE => "Service unavailable",
StatusCode::GATEWAY_TIMEOUT => "Gateway timeout",
StatusCode::INTERNAL_SERVER_ERROR => "An unexpected error occurred",
_ => "An unknown error occurred",
}
}
#[cfg(test)]
mod test_route_error {
use super::*;
use crate::RouteErrorOutput;
use anyhow::anyhow;
use axum::response::IntoResponse;
use http_body_util::BodyExt;
use serde_json::from_slice;
#[tokio::test]
async fn it_should_not_output_internal_error() {
fn raise_error() -> Result<(), RouteError> {
Err(anyhow!("Too many foxes in the DB"))?;
Ok(())
}
let err = raise_error().unwrap_err();
let response = err.into_response();
let response_body = response.into_body();
let response_bytes = response_body.collect().await.unwrap().to_bytes();
let body = from_slice::<RouteErrorOutput<()>>(&response_bytes).unwrap();
assert_eq!(body.internal_error, None);
}
}