diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index c1c228451e..8e6045f06b 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -8,8 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased - **added:** Add `WebSocket::protocol` to return the selected WebSocket subprotocol, if there is one. ([#1022]) +- **fixed:** Improve error for `PathRejection::WrongNumberOfParameters` to hint at using + `Path<(String, String)>` or `Path` ([#1023]) +- **fixed:** `PathRejection::WrongNumberOfParameters` now uses `500 Internal Server Error` since + its a programmer error and not a client error ([#1023]) [#1022]: https://github.com/tokio-rs/axum/pull/1022 +[#1023]: https://github.com/tokio-rs/axum/pull/1023 # 0.5.5 (10. May, 2022) diff --git a/axum/src/extract/path/mod.rs b/axum/src/extract/path/mod.rs index 0dc0167b61..540ff8a730 100644 --- a/axum/src/extract/path/mod.rs +++ b/axum/src/extract/path/mod.rs @@ -66,8 +66,7 @@ use std::{ /// ``` /// /// Path segments also can be deserialized into any type that implements -/// [`serde::Deserialize`]. Path segment labels will be matched with struct -/// field names. +/// [`serde::Deserialize`]. This includes tuples and structs: /// /// ```rust,no_run /// use axum::{ @@ -78,6 +77,7 @@ use std::{ /// use serde::Deserialize; /// use uuid::Uuid; /// +/// // Path segment labels will be matched with struct field names /// #[derive(Deserialize)] /// struct Params { /// user_id: Uuid, @@ -90,7 +90,17 @@ use std::{ /// // ... /// } /// -/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show)); +/// // When using tuples the path segments will be matched by their position in the route +/// async fn users_teams_create( +/// Path((user_id, team_id)): Path<(String, String)>, +/// ) { +/// // ... +/// } +/// +/// let app = Router::new().route( +/// "/users/:user_id/team/:team_id", +/// get(users_teams_show).post(users_teams_create), +/// ); /// # async { /// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); /// # }; @@ -319,11 +329,19 @@ impl fmt::Display for ErrorKind { match self { ErrorKind::Message(error) => error.fmt(f), ErrorKind::InvalidUtf8InPathParam { key } => write!(f, "Invalid UTF-8 in `{}`", key), - ErrorKind::WrongNumberOfParameters { got, expected } => write!( - f, - "Wrong number of parameters. Expected {} but got {}", - expected, got - ), + ErrorKind::WrongNumberOfParameters { got, expected } => { + write!( + f, + "Wrong number of path arguments for `Path`. Expected {} but got {}", + expected, got + )?; + + if *expected == 1 { + write!(f, ". Note that multiple parameters must be extracted with a tuple `Path<(_, _)>` or a struct `Path`")?; + } + + Ok(()) + } ErrorKind::UnsupportedType { name } => write!(f, "Unsupported type `{}`", name), ErrorKind::ParseErrorAtKey { key, @@ -368,14 +386,13 @@ impl IntoResponse for FailedToDeserializePathParams { let (status, body) = match self.0.kind { ErrorKind::Message(_) | ErrorKind::InvalidUtf8InPathParam { .. } - | ErrorKind::WrongNumberOfParameters { .. } | ErrorKind::ParseError { .. } | ErrorKind::ParseErrorAtIndex { .. } | ErrorKind::ParseErrorAtKey { .. } => ( StatusCode::BAD_REQUEST, format!("Invalid URL: {}", self.0.kind), ), - ErrorKind::UnsupportedType { .. } => { + ErrorKind::WrongNumberOfParameters { .. } | ErrorKind::UnsupportedType { .. } => { (StatusCode::INTERNAL_SERVER_ERROR, self.0.kind.to_string()) } }; @@ -539,4 +556,19 @@ mod tests { let res = client.get("/foo%20bar").send().await; assert_eq!(res.text().await, "foo bar"); } + + #[tokio::test] + async fn two_path_extractors() { + let app = Router::new().route("/:a/:b", get(|_: Path, _: Path| async {})); + + let client = TestClient::new(app); + + let res = client.get("/a/b").send().await; + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + res.text().await, + "Wrong number of path arguments for `Path`. Expected 1 but got 2. \ + Note that multiple parameters must be extracted with a tuple `Path<(_, _)>` or a struct `Path`", + ); + } }