axum_extra/extract/
optional_path.rs

1use axum::{
2    async_trait,
3    extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Path},
4    RequestPartsExt,
5};
6use serde::de::DeserializeOwned;
7
8/// Extractor that extracts path arguments the same way as [`Path`], except if there aren't any.
9///
10/// This extractor can be used in place of `Path` when you have two routes that you want to handle
11/// in mostly the same way, where one has a path parameter and the other one doesn't.
12///
13/// # Example
14///
15/// ```
16/// use std::num::NonZeroU32;
17/// use axum::{
18///     response::IntoResponse,
19///     routing::get,
20///     Router,
21/// };
22/// use axum_extra::extract::OptionalPath;
23///
24/// async fn render_blog(OptionalPath(page): OptionalPath<NonZeroU32>) -> impl IntoResponse {
25///     // Convert to u32, default to page 1 if not specified
26///     let page = page.map_or(1, |param| param.get());
27///     // ...
28/// }
29///
30/// let app = Router::new()
31///     .route("/blog", get(render_blog))
32///     .route("/blog/:page", get(render_blog));
33/// # let app: Router = app;
34/// ```
35#[derive(Debug)]
36pub struct OptionalPath<T>(pub Option<T>);
37
38#[async_trait]
39impl<T, S> FromRequestParts<S> for OptionalPath<T>
40where
41    T: DeserializeOwned + Send + 'static,
42    S: Send + Sync,
43{
44    type Rejection = PathRejection;
45
46    async fn from_request_parts(
47        parts: &mut http::request::Parts,
48        _: &S,
49    ) -> Result<Self, Self::Rejection> {
50        match parts.extract::<Path<T>>().await {
51            Ok(Path(params)) => Ok(Self(Some(params))),
52            Err(PathRejection::FailedToDeserializePathParams(e))
53                if matches!(e.kind(), ErrorKind::WrongNumberOfParameters { got: 0, .. }) =>
54            {
55                Ok(Self(None))
56            }
57            Err(e) => Err(e),
58        }
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use std::num::NonZeroU32;
65
66    use axum::{routing::get, Router};
67
68    use super::OptionalPath;
69    use crate::test_helpers::TestClient;
70
71    #[crate::test]
72    async fn supports_128_bit_numbers() {
73        async fn handle(OptionalPath(param): OptionalPath<NonZeroU32>) -> String {
74            let num = param.map_or(0, |p| p.get());
75            format!("Success: {num}")
76        }
77
78        let app = Router::new()
79            .route("/", get(handle))
80            .route("/:num", get(handle));
81
82        let client = TestClient::new(app);
83
84        let res = client.get("/").await;
85        assert_eq!(res.text().await, "Success: 0");
86
87        let res = client.get("/1").await;
88        assert_eq!(res.text().await, "Success: 1");
89
90        let res = client.get("/0").await;
91        assert_eq!(
92            res.text().await,
93            "Invalid URL: invalid value: integer `0`, expected a nonzero u32"
94        );
95
96        let res = client.get("/NaN").await;
97        assert_eq!(
98            res.text().await,
99            "Invalid URL: Cannot parse `\"NaN\"` to a `u32`"
100        );
101    }
102}