use std::borrow::Cow;
use std::fmt;
use axum_core::response::{IntoResponse, Response};
use http::StatusCode;
use http::header::{self, HeaderName, HeaderValue};
use serde::Serialize;
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub const INTERNAL_ERROR_MESSAGE: &str = "An internal server error occurred";
pub struct Error {
inner: Box<Inner>,
}
struct Inner {
status: StatusCode,
code: Cow<'static, str>,
message: Cow<'static, str>,
details: Option<serde_json::Value>,
headers: Vec<(HeaderName, HeaderValue)>,
source: Option<BoxError>,
}
macro_rules! constructors {
($( $(#[$doc:meta])* $name:ident => $status:ident, $code:literal; )*) => {
$(
$(#[$doc])*
pub fn $name(message: impl Into<Cow<'static, str>>) -> Self {
Self::new(StatusCode::$status, $code, message)
}
)*
};
}
impl Error {
pub fn new(
status: StatusCode,
code: impl Into<Cow<'static, str>>,
message: impl Into<Cow<'static, str>>,
) -> Self {
Self {
inner: Box::new(Inner {
status,
code: code.into(),
message: message.into(),
details: None,
headers: Vec::new(),
source: None,
}),
}
}
constructors! {
bad_request => BAD_REQUEST, "BAD_REQUEST";
unauthorized => UNAUTHORIZED, "UNAUTHORIZED";
forbidden => FORBIDDEN, "FORBIDDEN";
not_found => NOT_FOUND, "NOT_FOUND";
method_not_allowed => METHOD_NOT_ALLOWED, "METHOD_NOT_ALLOWED";
timeout => REQUEST_TIMEOUT, "REQUEST_TIMEOUT";
conflict => CONFLICT, "CONFLICT";
payload_too_large => PAYLOAD_TOO_LARGE, "PAYLOAD_TOO_LARGE";
unsupported_media_type => UNSUPPORTED_MEDIA_TYPE, "UNSUPPORTED_MEDIA_TYPE";
unprocessable => UNPROCESSABLE_ENTITY, "UNPROCESSABLE_ENTITY";
too_many_requests => TOO_MANY_REQUESTS, "TOO_MANY_REQUESTS";
service_unavailable => SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE";
}
pub fn validation(details: impl Serialize) -> Self {
Self::new(
StatusCode::UNPROCESSABLE_ENTITY,
"VALIDATION_FAILED",
"The request failed validation",
)
.with_details(details)
}
pub fn internal(source: impl Into<BoxError>) -> Self {
Self::new(
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
INTERNAL_ERROR_MESSAGE,
)
.with_source(source)
}
#[must_use]
pub fn with_code(mut self, code: impl Into<Cow<'static, str>>) -> Self {
self.inner.code = code.into();
self
}
#[must_use]
pub fn with_message(mut self, message: impl Into<Cow<'static, str>>) -> Self {
self.inner.message = message.into();
self
}
#[must_use]
pub fn with_details(mut self, details: impl Serialize) -> Self {
match serde_json::to_value(details) {
Ok(value) => self.inner.details = Some(value),
Err(err) => tracing::warn!(error = %err, "failed to serialize error details"),
}
self
}
#[must_use]
pub fn with_source(mut self, source: impl Into<BoxError>) -> Self {
self.inner.source = Some(source.into());
self
}
#[must_use]
pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.inner.headers.push((name, value));
self
}
pub fn status(&self) -> StatusCode {
self.inner.status
}
pub fn code(&self) -> &str {
&self.inner.code
}
pub fn message(&self) -> &str {
&self.inner.message
}
pub fn details(&self) -> Option<&serde_json::Value> {
self.inner.details.as_ref()
}
pub fn headers(&self) -> &[(HeaderName, HeaderValue)] {
&self.inner.headers
}
pub fn source_ref(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
self.inner.source.as_deref()
}
pub fn to_json_body(&self) -> Vec<u8> {
#[derive(Serialize)]
struct Envelope<'a> {
error: Body<'a>,
}
#[derive(Serialize)]
struct Body<'a> {
code: &'a str,
message: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<&'a serde_json::Value>,
}
let envelope = Envelope {
error: Body {
code: &self.inner.code,
message: &self.inner.message,
details: self.inner.details.as_ref(),
},
};
serde_json::to_vec(&envelope).unwrap_or_default()
}
fn log(&self) {
let status = self.inner.status.as_u16();
let code: &str = &self.inner.code;
match (&self.inner.source, self.inner.status.is_server_error()) {
(Some(source), true) => {
tracing::error!(status, code, error = %ErrorChain(source.as_ref()), "request failed")
}
(None, true) => {
tracing::error!(status, code, message = %self.inner.message, "request failed")
}
(Some(source), false) => {
tracing::debug!(status, code, error = %ErrorChain(source.as_ref()), "request rejected")
}
(None, false) => tracing::debug!(status, code, "request rejected"),
}
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
self.log();
let body = self.to_json_body();
let Inner {
status, headers, ..
} = *self.inner;
let mut response = (status, body).into_response();
let map = response.headers_mut();
map.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
for (name, value) in headers {
map.append(name, value);
}
response
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {}: {}",
self.inner.status.as_u16(),
self.inner.code,
self.inner.message
)?;
if let Some(source) = &self.inner.source {
write!(f, ": {}", ErrorChain(source.as_ref()))?;
}
Ok(())
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_struct("Error")
.field("status", &self.inner.status)
.field("code", &self.inner.code)
.field("message", &self.inner.message)
.field("details", &self.inner.details)
.field("source", &self.inner.source)
.finish()
} else {
fmt::Display::fmt(self, f)
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.inner
.source
.as_ref()
.map(|s| s.as_ref() as &(dyn std::error::Error + 'static))
}
}
impl From<StatusCode> for Error {
fn from(status: StatusCode) -> Self {
let reason = status.canonical_reason().unwrap_or("Error");
let code = reason.to_ascii_uppercase().replace([' ', '-'], "_");
Self::new(status, code, reason)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self::internal(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::internal(err)
}
}
impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
Self::internal(err)
}
}
impl From<crate::ServerError> for Error {
fn from(err: crate::ServerError) -> Self {
Self::new(
StatusCode::INTERNAL_SERVER_ERROR,
"SERVER_ERROR",
INTERNAL_ERROR_MESSAGE,
)
.with_source(err)
}
}
impl From<crate::ConfigError> for Error {
fn from(err: crate::ConfigError) -> Self {
Self::new(
StatusCode::INTERNAL_SERVER_ERROR,
"CONFIG_ERROR",
INTERNAL_ERROR_MESSAGE,
)
.with_source(err)
}
}
struct ErrorChain<'a>(&'a (dyn std::error::Error + 'static));
impl fmt::Display for ErrorChain<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)?;
let mut current = self.0.source();
while let Some(source) = current {
write!(f, ": {source}")?;
current = source.source();
}
Ok(())
}
}
pub trait ResultExt<T> {
fn or_internal(self) -> Result<T>;
fn or_error(self, error: Error) -> Result<T>;
}
impl<T, E> ResultExt<T> for std::result::Result<T, E>
where
E: Into<BoxError>,
{
fn or_internal(self) -> Result<T> {
self.map_err(Error::internal)
}
fn or_error(self, error: Error) -> Result<T> {
self.map_err(|source| error.with_source(source))
}
}
pub trait OptionExt<T> {
fn or_not_found(self, message: impl Into<Cow<'static, str>>) -> Result<T>;
fn or_error(self, error: Error) -> Result<T>;
}
impl<T> OptionExt<T> for Option<T> {
fn or_not_found(self, message: impl Into<Cow<'static, str>>) -> Result<T> {
self.ok_or_else(|| Error::not_found(message))
}
fn or_error(self, error: Error) -> Result<T> {
self.ok_or(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use http_body_util::BodyExt;
async fn body_json(response: Response) -> serde_json::Value {
let bytes = response.into_body().collect().await.unwrap().to_bytes();
serde_json::from_slice(&bytes).unwrap()
}
#[test]
fn error_is_pointer_sized() {
assert_eq!(std::mem::size_of::<Error>(), std::mem::size_of::<usize>());
}
#[tokio::test]
async fn renders_standard_envelope() {
let err = Error::not_found("The requested user does not exist").with_code("USER_NOT_FOUND");
let response = err.into_response();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
assert_eq!(response.headers()[header::CONTENT_TYPE], "application/json");
let body = body_json(response).await;
assert_eq!(
body,
serde_json::json!({"error": {"code": "USER_NOT_FOUND", "message": "The requested user does not exist"}})
);
}
#[tokio::test]
async fn internal_errors_never_expose_their_source() {
let source = std::io::Error::other("connection to db.internal:5432 refused");
let response = Error::internal(source).into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
let text = body_json(response).await.to_string();
assert!(!text.contains("db.internal"));
assert!(text.contains(INTERNAL_ERROR_MESSAGE));
}
#[tokio::test]
async fn validation_details_and_headers() {
let response = Error::validation(serde_json::json!({"email": ["invalid"]}))
.with_header(header::RETRY_AFTER, HeaderValue::from_static("1"))
.into_response();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
assert_eq!(response.headers()[header::RETRY_AFTER], "1");
let body = body_json(response).await;
assert_eq!(body["error"]["code"], "VALIDATION_FAILED");
assert_eq!(body["error"]["details"]["email"][0], "invalid");
}
#[test]
fn from_status_code() {
let err = Error::from(StatusCode::IM_A_TEAPOT);
assert_eq!(err.code(), "I'M_A_TEAPOT");
assert_eq!(err.status(), StatusCode::IM_A_TEAPOT);
let err = Error::from(StatusCode::GATEWAY_TIMEOUT);
assert_eq!(err.code(), "GATEWAY_TIMEOUT");
}
#[test]
fn result_and_option_extensions() {
let r: std::result::Result<(), std::io::Error> = Err(std::io::Error::other("boom"));
let err = r.or_internal().unwrap_err();
assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(err.to_string().contains("boom"));
let err = None::<u8>.or_not_found("missing").unwrap_err();
assert_eq!(err.status(), StatusCode::NOT_FOUND);
assert_eq!(err.message(), "missing");
}
#[test]
fn domain_error_conversion_with_question_mark() {
#[derive(Debug)]
struct Missing;
impl From<Missing> for Error {
fn from(_: Missing) -> Self {
Error::not_found("gone").with_code("GONE")
}
}
fn handler() -> Result<()> {
Err(Missing)?;
Ok(())
}
assert_eq!(handler().unwrap_err().code(), "GONE");
}
}