From 1d2a674e95f2022dd94bd2b6ada42a8c099f0585 Mon Sep 17 00:00:00 2001 From: Paul Grandperrin Date: Wed, 20 Mar 2024 23:20:41 +0100 Subject: [PATCH] Add new traits and types to improve error handling user experience. 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>` 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. --- axum-core/src/error_response.rs | 39 ++++++++++++++++++ axum-core/src/ext_traits/mod.rs | 1 + axum-core/src/ext_traits/result.rs | 20 ++++++++++ axum-core/src/lib.rs | 4 +- axum-core/src/response/into_response.rs | 39 +----------------- .../src/response/into_result_response.rs | 18 +++++++++ axum-core/src/response/mod.rs | 2 + axum/src/lib.rs | 2 +- axum/src/response/mod.rs | 2 +- examples/anyhow-error-response/src/main.rs | 40 +++++-------------- 10 files changed, 98 insertions(+), 69 deletions(-) create mode 100644 axum-core/src/error_response.rs create mode 100644 axum-core/src/ext_traits/result.rs create mode 100644 axum-core/src/response/into_result_response.rs diff --git a/axum-core/src/error_response.rs b/axum-core/src/error_response.rs new file mode 100644 index 0000000000..dc898629c7 --- /dev/null +++ b/axum-core/src/error_response.rs @@ -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, +} + +impl ErrorResponse { + /// Create a new `ErrorResponse` with the given status code and error. + pub fn new(status: StatusCode, error: impl Into>) -> Self { + Self { + status, + error: error.into(), + } + } +} + +impl>> From 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() + } +} diff --git a/axum-core/src/ext_traits/mod.rs b/axum-core/src/ext_traits/mod.rs index 02595fbeac..f07e3a8a74 100644 --- a/axum-core/src/ext_traits/mod.rs +++ b/axum-core/src/ext_traits/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod request; pub(crate) mod request_parts; +pub(crate) mod result; #[cfg(test)] mod tests { diff --git a/axum-core/src/ext_traits/result.rs b/axum-core/src/ext_traits/result.rs new file mode 100644 index 0000000000..e6816321e1 --- /dev/null +++ b/axum-core/src/ext_traits/result.rs @@ -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 { + /// maps the error type to a `ErrorResponse` with the given status code. + fn err_with_status(self, status: StatusCode) -> ResultResponse; +} + +impl>> ResultExt for std::result::Result { + fn err_with_status(self, status:StatusCode) -> ResultResponse { + self.map_err(|error| { + ErrorResponse::new(status, error) + }) + } +} diff --git a/axum-core/src/lib.rs b/axum-core/src/lib.rs index 994b522c07..8211315992 100644 --- a/axum-core/src/lib.rs +++ b/axum-core/src/lib.rs @@ -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; @@ -63,4 +65,4 @@ pub mod response; /// Alias for a type-erased error type. pub type BoxError = Box; -pub use self::ext_traits::{request::RequestExt, request_parts::RequestPartsExt}; +pub use self::ext_traits::{request::RequestExt, result::ResultExt, request_parts::RequestPartsExt}; diff --git a/axum-core/src/response/into_response.rs b/axum-core/src/response/into_response.rs index 679b0cbb74..83984eed3b 100644 --- a/axum-core/src/response/into_response.rs +++ b/axum-core/src/response/into_response.rs @@ -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` 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: diff --git a/axum-core/src/response/into_result_response.rs b/axum-core/src/response/into_result_response.rs new file mode 100644 index 0000000000..7a47128ac3 --- /dev/null +++ b/axum-core/src/response/into_result_response.rs @@ -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` aka `Result` +/// where both `T` and `ErrorResponse` implement `IntoResponse`. +/// +/// The trait allows to return a `Result` from a handler. +pub trait IntoResultResponse: IntoResponse {} +impl IntoResultResponse for ResultResponse {} + +/// A type alias for `Result`. +pub type ResultResponse = std::result::Result; diff --git a/axum-core/src/response/mod.rs b/axum-core/src/response/mod.rs index 6b66c60e71..73bddaf2b6 100644 --- a/axum-core/src/response/mod.rs +++ b/axum-core/src/response/mod.rs @@ -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 diff --git a/axum/src/lib.rs b/axum/src/lib.rs index 601c14ae74..657f0d83ec 100644 --- a/axum/src/lib.rs +++ b/axum/src/lib.rs @@ -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; diff --git a/axum/src/response/mod.rs b/axum/src/response/mod.rs index 6cfd9b0763..a4d439589e 100644 --- a/axum/src/response/mod.rs +++ b/axum/src/response/mod.rs @@ -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)] diff --git a/examples/anyhow-error-response/src/main.rs b/examples/anyhow-error-response/src/main.rs index b7a2416a4a..b01b827412 100644 --- a/examples/anyhow-error-response/src/main.rs +++ b/examples/anyhow-error-response/src/main.rs @@ -6,9 +6,9 @@ use axum::{ http::StatusCode, - response::{IntoResponse, Response}, + response::IntoResultResponse, routing::get, - Router, + Router, ResultExt, }; #[tokio::main] @@ -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 From for AppError -where - E: Into, -{ - fn from(err: E) -> Self { - Self(err.into()) - } +fn try_thing_stderror() -> Result<(), impl std::error::Error> { + Err(std::fmt::Error::default()) }