Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error handling user experience. #2665

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions axum-core/src/error_response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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);

#[cfg(feature = "tracing")]
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())
}
Loading