//! Run with //! //! ```not_rust //! cargo run -p example-customize-path-rejection //! ``` use axum::{ extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts}, http::{request::Parts, StatusCode}, response::IntoResponse, routing::get, Router, }; use serde::{de::DeserializeOwned, Deserialize, Serialize}; 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", env!("CARGO_CRATE_NAME")).into()), ) .with(tracing_subscriber::fmt::layer()) .init(); // build our application with a route let app = Router::new().route("/users/{user_id}/teams/{team_id}", get(handler)); // run it 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; } async fn handler(Path(params): Path) -> impl IntoResponse { axum::Json(params) } #[derive(Debug, Deserialize, Serialize)] struct Params { user_id: u32, team_id: u32, } // We define our own `Path` extractor that customizes the error from `axum::extract::Path` struct Path(T); impl FromRequestParts for Path where // these trait bounds are copied from `impl FromRequest for axum::extract::path::Path` T: DeserializeOwned + Send, S: Send + Sync, { type Rejection = (StatusCode, axum::Json); async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { match axum::extract::Path::::from_request_parts(parts, state).await { Ok(value) => Ok(Self(value.0)), Err(rejection) => { let (status, body) = match rejection { PathRejection::FailedToDeserializePathParams(inner) => { let mut status = StatusCode::BAD_REQUEST; let kind = inner.into_kind(); let body = match &kind { ErrorKind::WrongNumberOfParameters { .. } => PathError { message: kind.to_string(), location: None, }, ErrorKind::ParseErrorAtKey { key, .. } => PathError { message: kind.to_string(), location: Some(key.clone()), }, ErrorKind::ParseErrorAtIndex { index, .. } => PathError { message: kind.to_string(), location: Some(index.to_string()), }, ErrorKind::ParseError { .. } => PathError { message: kind.to_string(), location: None, }, ErrorKind::InvalidUtf8InPathParam { key } => PathError { message: kind.to_string(), location: Some(key.clone()), }, ErrorKind::UnsupportedType { .. } => { // this error is caused by the programmer using an unsupported type // (such as nested maps) so respond with `500` instead status = StatusCode::INTERNAL_SERVER_ERROR; PathError { message: kind.to_string(), location: None, } } ErrorKind::Message(msg) => PathError { message: msg.clone(), location: None, }, _ => PathError { message: format!("Unhandled deserialization error: {kind}"), location: None, }, }; (status, body) } PathRejection::MissingPathParams(error) => ( StatusCode::INTERNAL_SERVER_ERROR, PathError { message: error.to_string(), location: None, }, ), _ => ( StatusCode::INTERNAL_SERVER_ERROR, PathError { message: format!("Unhandled path rejection: {rejection}"), location: None, }, ), }; Err((status, axum::Json(body))) } } } } #[derive(Serialize)] struct PathError { message: String, location: Option, }