Skip to content

Commit

Permalink
Add new traits and types to improve error handling user experience.
Browse files Browse the repository at this point in the history
Adds the trait `IntoResultResponse` to be use in place of
`IntoResponse` in handlers to allow easy use of `?` on standard errors
and other implementations like `anyhow` and `eyre`.

The type `ErrorResponse` encapsulate any kind of error with an associated
`StatusCode` and the type alias `ResultResponse` use it by default in a
`Result`.

`ErrorResponse` can be converted from any `Into<Box<dyn Error>>` and
also implements `IntoResponse`.

`IntoResultResponse` is only implemented by `ResultResponse` to force the
type system to unambiguously convert any errors from handlers to
`ErrorResponse` when using `?`.

This commits only add new traits and types doesn't change any existing
API.
  • Loading branch information
PaulGrandperrin committed Mar 21, 2024
1 parent 2ec68d6 commit 1d2a674
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 69 deletions.
39 changes: 39 additions & 0 deletions axum-core/src/error_response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::error::Error;

use http::StatusCode;

use crate::response::{IntoResponse, Response};

/// `ErrorResponse` encapsulates an `Error` and a `StatusCode` to be used as a response, typically in a `Result`.
///
/// If not `StatusCode` is provided, `StatusCode::INTERNAL_SERVER_ERROR` is used.
#[derive(Debug)]
pub struct ErrorResponse {
status: StatusCode,
error: Box<dyn Error>,
}

impl ErrorResponse {
/// Create a new `ErrorResponse` with the given status code and error.
pub fn new(status: StatusCode, error: impl Into<Box<dyn Error>>) -> Self {
Self {
status,
error: error.into(),
}
}
}

impl<E: Into<Box<dyn Error>>> From<E> for ErrorResponse
{
fn from(error: E) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, error)
}
}

impl IntoResponse for ErrorResponse {
fn into_response(self) -> Response {
let error = format!("{:?}", self.error);
tracing::error!(error = %error);
(self.status, error).into_response()
}
}
1 change: 1 addition & 0 deletions axum-core/src/ext_traits/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub(crate) mod request;
pub(crate) mod request_parts;
pub(crate) mod result;

#[cfg(test)]
mod tests {
Expand Down
20 changes: 20 additions & 0 deletions axum-core/src/ext_traits/result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::error::Error;

use http::StatusCode;

use crate::{response::{IntoResponse, ResultResponse}, error_response::ErrorResponse};

/// A extention trait to Result to easily attach a `StatusCode` to an error by encapsulating the
/// error into a `ErrorResponse`.
pub trait ResultExt<T: IntoResponse> {
/// maps the error type to a `ErrorResponse` with the given status code.
fn err_with_status(self, status: StatusCode) -> ResultResponse<T>;
}

impl<T: IntoResponse, E: Into<Box<dyn Error>>> ResultExt<T> for std::result::Result<T, E> {
fn err_with_status(self, status:StatusCode) -> ResultResponse<T> {
self.map_err(|error| {
ErrorResponse::new(status, error)
})
}
}
4 changes: 3 additions & 1 deletion axum-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@
pub(crate) mod macros;

mod error;
mod error_response;
mod ext_traits;
pub use self::error::Error;
pub use self::error_response::ErrorResponse;

pub mod body;
pub mod extract;
Expand All @@ -63,4 +65,4 @@ pub mod response;
/// Alias for a type-erased error type.
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;

pub use self::ext_traits::{request::RequestExt, request_parts::RequestPartsExt};
pub use self::ext_traits::{request::RequestExt, result::ResultExt, request_parts::RequestPartsExt};
39 changes: 2 additions & 37 deletions axum-core/src/response/into_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,8 @@ use std::{
/// You generally shouldn't have to implement `IntoResponse` manually, as axum
/// provides implementations for many common types.
///
/// However it might be necessary if you have a custom error type that you want
/// to return from handlers:
///
/// ```rust
/// use axum::{
/// Router,
/// body::{self, Bytes},
/// routing::get,
/// http::StatusCode,
/// response::{IntoResponse, Response},
/// };
///
/// enum MyError {
/// SomethingWentWrong,
/// SomethingElseWentWrong,
/// }
///
/// impl IntoResponse for MyError {
/// fn into_response(self) -> Response {
/// let body = match self {
/// MyError::SomethingWentWrong => "something went wrong",
/// MyError::SomethingElseWentWrong => "something else went wrong",
/// };
///
/// // it's often easiest to implement `IntoResponse` by calling other implementations
/// (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
/// }
/// }
///
/// // `Result<impl IntoResponse, MyError>` can now be returned from handlers
/// let app = Router::new().route("/", get(handler));
///
/// async fn handler() -> Result<(), MyError> {
/// Err(MyError::SomethingWentWrong)
/// }
/// # let _: Router = app;
/// ```
/// To handle errors and return them as a `Result` from a handler, you can
/// use `IntoResultResponse` instead.
///
/// Or if you have a custom body type you'll also need to implement
/// `IntoResponse` for it:
Expand Down
18 changes: 18 additions & 0 deletions axum-core/src/response/into_result_response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use crate::error_response::ErrorResponse;

use super::IntoResponse;

/// Trait for generating fallible responses in handlers.
///
/// This trait is bound by `IntoResponse` and therefor can be be interchanged with it
/// when returning a `Result` from a handler.
///
/// This trait is only implemented for `ResultResponse<T>` aka `Result<T, ErrorResponse>`
/// where both `T` and `ErrorResponse` implement `IntoResponse`.
///
/// The trait allows to return a `Result` from a handler.
pub trait IntoResultResponse: IntoResponse {}
impl<T: IntoResponse> IntoResultResponse for ResultResponse<T> {}

/// A type alias for `Result<T, ErrorResponse>`.
pub type ResultResponse<T, E = ErrorResponse> = std::result::Result<T, E>;
2 changes: 2 additions & 0 deletions axum-core/src/response/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ use crate::body::Body;
mod append_headers;
mod into_response;
mod into_response_parts;
mod into_result_response;

pub use self::{
append_headers::AppendHeaders,
into_response::IntoResponse,
into_response_parts::{IntoResponseParts, ResponseParts, TryIntoHeaderError},
into_result_response::{IntoResultResponse, ResultResponse},
};

/// Type alias for [`http::Response`] whose body type defaults to [`Body`], the most common body
Expand Down
2 changes: 1 addition & 1 deletion axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ pub use self::routing::Router;
pub use self::form::Form;

#[doc(inline)]
pub use axum_core::{BoxError, Error, RequestExt, RequestPartsExt};
pub use axum_core::{BoxError, Error, ErrorResponse, RequestExt, RequestPartsExt, ResultExt};

#[cfg(feature = "macros")]
pub use axum_macros::debug_handler;
Expand Down
2 changes: 1 addition & 1 deletion axum/src/response/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub use crate::Extension;

#[doc(inline)]
pub use axum_core::response::{
AppendHeaders, ErrorResponse, IntoResponse, IntoResponseParts, Response, ResponseParts, Result,
AppendHeaders, ErrorResponse, IntoResponse, IntoResponseParts, IntoResultResponse, Response, ResponseParts, Result, ResultResponse,
};

#[doc(inline)]
Expand Down
40 changes: 11 additions & 29 deletions examples/anyhow-error-response/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

use axum::{
http::StatusCode,
response::{IntoResponse, Response},
response::IntoResultResponse,
routing::get,
Router,
Router, ResultExt,
};

#[tokio::main]
Expand All @@ -22,36 +22,18 @@ async fn main() {
axum::serve(listener, app).await.unwrap();
}

async fn handler() -> Result<(), AppError> {
try_thing()?;
Ok(())
}
async fn handler() -> impl IntoResultResponse {
try_thing_anyhow()?; // by default this will return a StatusCode::INTERNAL_SERVER_ERROR (500) error
try_thing_anyhow().err_with_status(StatusCode::BAD_REQUEST)?; // Using the `ResultExt` trait to return a StatusCode::BAD_REQUEST (400) error

fn try_thing() -> Result<(), anyhow::Error> {
anyhow::bail!("it failed!")
try_thing_stderror()?; // Standard errors also work
Ok(())
}

// Make our own error that wraps `anyhow::Error`.
struct AppError(anyhow::Error);

// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
fn try_thing_anyhow() -> Result<(), anyhow::Error> {
anyhow::bail!("it failed!");
}

// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
fn try_thing_stderror() -> Result<(), impl std::error::Error> {
Err(std::fmt::Error::default())
}

0 comments on commit 1d2a674

Please sign in to comment.