@@ -32,6 +32,7 @@ use std::{
3232use axum:: {
3333 extract:: { rejection:: JsonRejection , FromRequest , MatchedPath , Request , State } ,
3434 http:: StatusCode ,
35+ middleware:: { from_fn, Next } ,
3536 response:: { IntoResponse , Response } ,
3637 routing:: post,
3738 Router ,
@@ -77,6 +78,7 @@ async fn main() {
7778 // logging of errors so disable that
7879 . on_failure ( ( ) ) ,
7980 )
81+ . layer ( from_fn ( log_app_errors) )
8082 . with_state ( state) ;
8183
8284 let listener = tokio:: net:: TcpListener :: bind ( "127.0.0.1:3000" )
@@ -145,6 +147,7 @@ where
145147}
146148
147149// The kinds of errors we can hit in our application.
150+ #[ derive( Debug ) ]
148151enum AppError {
149152 // The request body contained invalid JSON
150153 JsonRejection ( JsonRejection ) ,
@@ -153,8 +156,6 @@ enum AppError {
153156}
154157
155158// Tell axum how `AppError` should be converted into a response.
156- //
157- // This is also a convenient place to log errors.
158159impl IntoResponse for AppError {
159160 fn into_response ( self ) -> Response {
160161 // How we want errors responses to be serialized
@@ -163,30 +164,36 @@ impl IntoResponse for AppError {
163164 message : String ,
164165 }
165166
166- let ( status, message) = match self {
167+ let ( status, message, err ) = match & self {
167168 AppError :: JsonRejection ( rejection) => {
168169 // This error is caused by bad user input so don't log it
169- ( rejection. status ( ) , rejection. body_text ( ) )
170+ ( rejection. status ( ) , rejection. body_text ( ) , None )
170171 }
171- AppError :: TimeError ( err) => {
172- // Because `TraceLayer` wraps each request in a span that contains the request
173- // method, uri, etc we don't need to include those details here
174- tracing:: error!( %err, "error from time_library" ) ;
175-
172+ AppError :: TimeError ( _err) => {
173+ // While we could simply log the error here we would introduce
174+ // a side-effect to our conversion, instead add the AppError to
175+ // the Response as an Extension
176176 // Don't expose any details about the error to the client
177177 (
178178 StatusCode :: INTERNAL_SERVER_ERROR ,
179179 "Something went wrong" . to_owned ( ) ,
180+ Some ( self ) ,
180181 )
181182 }
182183 } ;
183184
184- ( status, AppJson ( ErrorResponse { message } ) ) . into_response ( )
185+ let mut response = ( status, AppJson ( ErrorResponse { message } ) ) . into_response ( ) ;
186+ if let Some ( err) = err {
187+ // Insert our error into the response, our logging middleware will use this.
188+ response. extensions_mut ( ) . insert ( Arc :: new ( err) ) ;
189+ }
190+ response
185191 }
186192}
187193
188194impl From < JsonRejection > for AppError {
189195 fn from ( rejection : JsonRejection ) -> Self {
196+ // Arc enables cloning of a JsonRejection
190197 Self :: JsonRejection ( rejection)
191198 }
192199}
@@ -197,6 +204,16 @@ impl From<time_library::Error> for AppError {
197204 }
198205}
199206
207+ // Our middleware is responsible for logging error details internally
208+ async fn log_app_errors ( request : Request , next : Next ) -> Response {
209+ let response = next. run ( request) . await ;
210+ // If the response contains an AppError Extension, log it.
211+ if let Some ( err) = response. extensions ( ) . get :: < Arc < AppError > > ( ) {
212+ tracing:: error!( ?err, "an unexpected error occurred inside a handler" ) ;
213+ }
214+ response
215+ }
216+
200217// Imagine this is some third party library that we're using. It sometimes returns errors which we
201218// want to log.
202219mod time_library {
@@ -220,7 +237,7 @@ mod time_library {
220237 }
221238 }
222239
223- #[ derive( Debug ) ]
240+ #[ derive( Debug , Clone ) ]
224241 pub enum Error {
225242 FailedToGetTime ,
226243 }
0 commit comments