//! Example showing how to convert errors into responses. //! //! Run with //! //! ```not_rust //! cargo run -p example-error-handling //! ``` //! //! For successful requests the log output will be //! //! ```ignore //! DEBUG request{method=POST uri=/users matched_path="/users"}: tower_http::trace::on_request: started processing request //! DEBUG request{method=POST uri=/users matched_path="/users"}: tower_http::trace::on_response: finished processing request latency=0 ms status=200 //! ``` //! //! For failed requests the log output will be //! //! ```ignore //! DEBUG request{method=POST uri=/users matched_path="/users"}: tower_http::trace::on_request: started processing request //! ERROR request{method=POST uri=/users matched_path="/users"}: example_error_handling: error from time_library err=failed to get time //! DEBUG request{method=POST uri=/users matched_path="/users"}: tower_http::trace::on_response: finished processing request latency=0 ms status=500 //! ``` use std::{ collections::HashMap, sync::{ atomic::{AtomicU64, Ordering}, Arc, Mutex, }, }; use axum::{ extract::{rejection::JsonRejection, FromRequest, MatchedPath, Request, State}, http::StatusCode, middleware::{from_fn, Next}, response::{IntoResponse, Response}, routing::post, Router, }; use serde::{Deserialize, Serialize}; use time_library::Timestamp; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { format!("{}=debug,tower_http=debug", env!("CARGO_CRATE_NAME")).into() }), ) .with(tracing_subscriber::fmt::layer()) .init(); let state = AppState::default(); let app = Router::new() // A dummy route that accepts some JSON but sometimes fails .route("/users", post(users_create)) .layer( TraceLayer::new_for_http() // Create our own span for the request and include the matched path. The matched // path is useful for figuring out which handler the request was routed to. .make_span_with(|req: &Request| { let method = req.method(); let uri = req.uri(); // axum automatically adds this extension. let matched_path = req .extensions() .get::() .map(|matched_path| matched_path.as_str()); tracing::debug_span!("request", %method, %uri, matched_path) }) // By default `TraceLayer` will log 5xx responses but we're doing our specific // 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") .await .unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await; } #[derive(Default, Clone)] struct AppState { next_id: Arc, users: Arc>>, } #[derive(Deserialize)] struct UserParams { name: String, } #[derive(Serialize, Clone)] struct User { id: u64, name: String, created_at: Timestamp, } async fn users_create( State(state): State, // Make sure to use our own JSON extractor so we get input errors formatted in a way that // matches our application AppJson(params): AppJson, ) -> Result, AppError> { let id = state.next_id.fetch_add(1, Ordering::SeqCst); // We have implemented `From for AppError` which allows us to use `?` to // automatically convert the error let created_at = Timestamp::now()?; let user = User { id, name: params.name, created_at, }; state.users.lock().unwrap().insert(id, user.clone()); Ok(AppJson(user)) } // Create our own JSON extractor by wrapping `axum::Json`. This makes it easy to override the // rejection and provide our own which formats errors to match our application. // // `axum::Json` responds with plain text if the input is invalid. #[derive(FromRequest)] #[from_request(via(axum::Json), rejection(AppError))] struct AppJson(T); impl IntoResponse for AppJson where axum::Json: IntoResponse, { fn into_response(self) -> Response { axum::Json(self.0).into_response() } } // The kinds of errors we can hit in our application. #[derive(Debug)] enum AppError { // The request body contained invalid JSON JsonRejection(JsonRejection), // Some error from a third party library we're using TimeError(time_library::Error), } // Tell axum how `AppError` should be converted into a response. impl IntoResponse for AppError { fn into_response(self) -> Response { // How we want errors responses to be serialized #[derive(Serialize)] struct ErrorResponse { message: String, } 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(), None) } 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), ) } }; 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. // By wrapping the error in an Arc we can use it as an Extension regardless of any inner types not deriving Clone. response.extensions_mut().insert(Arc::new(err)); } response } } impl From for AppError { fn from(rejection: JsonRejection) -> Self { Self::JsonRejection(rejection) } } impl From for AppError { fn from(error: time_library::Error) -> Self { Self::TimeError(error) } } // 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 { use std::sync::atomic::{AtomicU64, Ordering}; use serde::Serialize; #[derive(Serialize, Clone)] pub struct Timestamp(u64); impl Timestamp { pub fn now() -> Result { static COUNTER: AtomicU64 = AtomicU64::new(0); // Fail on every third call just to simulate errors if COUNTER.fetch_add(1, Ordering::SeqCst).is_multiple_of(3) { Err(Error::FailedToGetTime) } else { Ok(Self(1337)) } } } #[derive(Debug, Clone)] pub enum Error { FailedToGetTime, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "failed to get time") } } }