Skip to content

Commit 8389ab1

Browse files
authored
examples: refactor error logging in error-handling example (#3527)
1 parent dcbcf5c commit 8389ab1

File tree

1 file changed

+28
-11
lines changed

1 file changed

+28
-11
lines changed

examples/error-handling/src/main.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use std::{
3232
use 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)]
148151
enum 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.
158159
impl 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

188194
impl 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.
202219
mod 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

Comments
 (0)