diff --git a/examples/error-handling/src/main.rs b/examples/error-handling/src/main.rs index 3cbcd47c..01ceeeb7 100644 --- a/examples/error-handling/src/main.rs +++ b/examples/error-handling/src/main.rs @@ -32,6 +32,7 @@ use std::{ use axum::{ extract::{rejection::JsonRejection, FromRequest, MatchedPath, Request, State}, http::StatusCode, + middleware::{from_fn, Next}, response::{IntoResponse, Response}, routing::post, Router, @@ -77,6 +78,7 @@ async fn main() { // logging of errors so disable that .on_failure(()), ) + .layer(from_fn(log_app_errors)) .with_state(state); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") @@ -145,6 +147,7 @@ where } // The kinds of errors we can hit in our application. +#[derive(Debug)] enum AppError { // The request body contained invalid JSON JsonRejection(JsonRejection), @@ -153,8 +156,6 @@ enum AppError { } // Tell axum how `AppError` should be converted into a response. -// -// This is also a convenient place to log errors. impl IntoResponse for AppError { fn into_response(self) -> Response { // How we want errors responses to be serialized @@ -163,30 +164,36 @@ impl IntoResponse for AppError { message: String, } - let (status, message) = match self { + let (status, message, err) = match &self { AppError::JsonRejection(rejection) => { // This error is caused by bad user input so don't log it - (rejection.status(), rejection.body_text()) + (rejection.status(), rejection.body_text(), None) } - AppError::TimeError(err) => { - // Because `TraceLayer` wraps each request in a span that contains the request - // method, uri, etc we don't need to include those details here - tracing::error!(%err, "error from time_library"); - + AppError::TimeError(_err) => { + // While we could simply log the error here we would introduce + // a side-effect to our conversion, instead add the AppError to + // the Response as an Extension // Don't expose any details about the error to the client ( StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong".to_owned(), + Some(self), ) } }; - (status, AppJson(ErrorResponse { message })).into_response() + let mut response = (status, AppJson(ErrorResponse { message })).into_response(); + if let Some(err) = err { + // Insert our error into the response, our logging middleware will use this. + response.extensions_mut().insert(Arc::new(err)); + } + response } } impl From for AppError { fn from(rejection: JsonRejection) -> Self { + // Arc enables cloning of a JsonRejection Self::JsonRejection(rejection) } } @@ -197,6 +204,16 @@ impl From for AppError { } } +// Our middleware is responsible for logging error details internally +async fn log_app_errors(request: Request, next: Next) -> Response { + let response = next.run(request).await; + // If the response contains an AppError Extension, log it. + if let Some(err) = response.extensions().get::>() { + tracing::error!(?err, "an unexpected error occurred inside a handler"); + } + response +} + // Imagine this is some third party library that we're using. It sometimes returns errors which we // want to log. mod time_library { @@ -220,7 +237,7 @@ mod time_library { } } - #[derive(Debug)] + #[derive(Debug, Clone)] pub enum Error { FailedToGetTime, }