From a2e64a8fc335d5ea8fd9ce191ceccb4f5944b68a Mon Sep 17 00:00:00 2001 From: Yunus <112053367+yunz-dev@users.noreply.github.com> Date: Tue, 4 Aug 2026 05:29:39 +1000 Subject: [PATCH] feat: add QUERY method routing (#3801) --- Cargo.lock | 4 ++-- axum-extra/CHANGELOG.md | 2 ++ axum-extra/src/routing/mod.rs | 23 +++++++++++++++++++++++ axum/CHANGELOG.md | 2 ++ axum/Cargo.toml | 2 +- axum/src/routing/method_filter.rs | 26 +++++++++++++++++--------- axum/src/routing/method_routing.rs | 29 ++++++++++++++++++++++++++++- axum/src/routing/mod.rs | 2 +- examples/Cargo.lock | 4 ++-- 9 files changed, 78 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41ffde3c..b9eb9463 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index 95820146..fc036e99 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -22,11 +22,13 @@ and this project adheres to [Semantic Versioning]. .route_with_tsr("/path", get(/* handler */)) .route_with_tsr("/path", post(/* handler */)) ``` +- **added:** Add `RouterExt::typed_query` ([#3801]) - **fixed:** Escape multipart `Content-Disposition` parameters and reject newlines in field names and filenames ([#3776]) [#3599]: https://github.com/tokio-rs/axum/pull/3599 [#3586]: https://github.com/tokio-rs/axum/pull/3586 +[#3801]: https://github.com/tokio-rs/axum/pull/3801 [#3776]: https://github.com/tokio-rs/axum/pull/3776 # 0.12.6 diff --git a/axum-extra/src/routing/mod.rs b/axum-extra/src/routing/mod.rs index 3b3cdc67..31d203bc 100644 --- a/axum-extra/src/routing/mod.rs +++ b/axum-extra/src/routing/mod.rs @@ -234,6 +234,19 @@ pub trait RouterExt: sealed::Sealed { T: SecondElementIs

+ 'static, P: TypedPath; + /// Add a typed `QUERY` route to the router. + /// + /// The path will be inferred from the first argument to the handler function which must + /// implement [`TypedPath`]. + /// + /// See [`TypedPath`] for more details and examples. + #[cfg(feature = "typed-routing")] + fn typed_query(self, handler: H) -> Self + where + H: axum::handler::Handler, + T: SecondElementIs

+ 'static, + P: TypedPath; + /// Add another route to the router with an additional "trailing slash redirect" route. /// /// If you add a route _without_ a trailing slash, such as `/foo`, this method will also add a @@ -368,6 +381,16 @@ where self.route(P::PATH, axum::routing::connect(handler)) } + #[cfg(feature = "typed-routing")] + fn typed_query(self, handler: H) -> Self + where + H: axum::handler::Handler, + T: SecondElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::query(handler)) + } + #[track_caller] fn route_with_tsr(mut self, path: &str, method_router: MethodRouter) -> Self where diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index 4327e8c8..934ffc0b 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **changed:** `Redirect` constructors now accept any `impl Into` ([#3635]) - **changed:** Updated `matchit` allowing for routes with captures and static prefixes and suffixes ([#3702]) - **fixed:** Responses to `HEAD` will not accidentally reply with `content-length: 0` anymore ([#3742]) +- **added:** Add `MethodFilter::QUERY`, `routing::query[_service]` and `MethodRouter::query[_service]` ([#3801]) - **fixed:** `MethodRouter::merge` no longer lists a method twice in the `Allow` header after merging `get` and `head` ([#3836]) @@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#3742]: https://github.com/tokio-rs/axum/pull/3742 [#3836]: https://github.com/tokio-rs/axum/pull/3836 [#3757]: https://github.com/tokio-rs/axum/pull/3757 +[#3801]: https://github.com/tokio-rs/axum/pull/3801 # 0.8.9 diff --git a/axum/Cargo.toml b/axum/Cargo.toml index 54e01bf5..8d02eb26 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -112,7 +112,7 @@ axum-core = { path = "../axum-core", version = "0.5.6" } bytes = "1.7" futures-core = "0.3" futures-util = { version = "0.3", default-features = false, features = ["alloc"] } -http = "1.0.0" +http = "1.5.0" http-body = "1.0.0" http-body-util = "0.1.0" itoa = "1.0.5" diff --git a/axum/src/routing/method_filter.rs b/axum/src/routing/method_filter.rs index 11d5c36d..527d2731 100644 --- a/axum/src/routing/method_filter.rs +++ b/axum/src/routing/method_filter.rs @@ -26,23 +26,25 @@ impl MethodFilter { /// [extended CONNECT]: https://www.rfc-editor.org/rfc/rfc8441.html#section-4 /// [HTTP Upgrade Token Registry]: https://www.iana.org/assignments/http-upgrade-tokens/http-upgrade-tokens.xhtml /// [`WebSocketUpgrade`]: crate::extract::WebSocketUpgrade - pub const CONNECT: Self = Self::from_bits(0b0_0000_0001); + pub const CONNECT: Self = Self::from_bits(0b00_0000_0001); /// Match `DELETE` requests. - pub const DELETE: Self = Self::from_bits(0b0_0000_0010); + pub const DELETE: Self = Self::from_bits(0b00_0000_0010); /// Match `GET` requests. - pub const GET: Self = Self::from_bits(0b0_0000_0100); + pub const GET: Self = Self::from_bits(0b00_0000_0100); /// Match `HEAD` requests. - pub const HEAD: Self = Self::from_bits(0b0_0000_1000); + pub const HEAD: Self = Self::from_bits(0b00_0000_1000); /// Match `OPTIONS` requests. - pub const OPTIONS: Self = Self::from_bits(0b0_0001_0000); + pub const OPTIONS: Self = Self::from_bits(0b00_0001_0000); /// Match `PATCH` requests. - pub const PATCH: Self = Self::from_bits(0b0_0010_0000); + pub const PATCH: Self = Self::from_bits(0b00_0010_0000); /// Match `POST` requests. - pub const POST: Self = Self::from_bits(0b0_0100_0000); + pub const POST: Self = Self::from_bits(0b00_0100_0000); /// Match `PUT` requests. - pub const PUT: Self = Self::from_bits(0b0_1000_0000); + pub const PUT: Self = Self::from_bits(0b00_1000_0000); /// Match `TRACE` requests. - pub const TRACE: Self = Self::from_bits(0b1_0000_0000); + pub const TRACE: Self = Self::from_bits(0b01_0000_0000); + /// Match `QUERY` requests. + pub const QUERY: Self = Self::from_bits(0b10_0000_0000); const fn bits(self) -> u16 { let bits = self; @@ -99,6 +101,7 @@ impl TryFrom for MethodFilter { Method::POST => Ok(Self::POST), Method::PUT => Ok(Self::PUT), Method::TRACE => Ok(Self::TRACE), + Method::QUERY => Ok(Self::QUERY), other => Err(NoMatchingMethodFilter { method: other }), } } @@ -155,6 +158,11 @@ mod tests { MethodFilter::TRACE ); + assert_eq!( + MethodFilter::try_from(Method::QUERY).unwrap(), + MethodFilter::QUERY + ); + assert!( MethodFilter::try_from(http::Method::from_bytes(b"CUSTOM").unwrap()) .unwrap_err() diff --git a/axum/src/routing/method_routing.rs b/axum/src/routing/method_routing.rs index 59e080f8..140507ef 100644 --- a/axum/src/routing/method_routing.rs +++ b/axum/src/routing/method_routing.rs @@ -341,6 +341,7 @@ top_level_service_fn!(patch_service, PATCH); top_level_service_fn!(post_service, POST); top_level_service_fn!(put_service, PUT); top_level_service_fn!(trace_service, TRACE); +top_level_service_fn!(query_service, QUERY); /// Route requests with the given method to the service. /// @@ -445,6 +446,7 @@ top_level_handler_fn!(patch, PATCH); top_level_handler_fn!(post, POST); top_level_handler_fn!(put, PUT); top_level_handler_fn!(trace, TRACE); +top_level_handler_fn!(query, QUERY); /// Route requests with the given method to the handler. /// @@ -554,6 +556,7 @@ pub struct MethodRouter { put: MethodEndpoint, trace: MethodEndpoint, connect: MethodEndpoint, + query: MethodEndpoint, fallback: Fallback, allow_header: AllowHeader, } @@ -604,6 +607,7 @@ impl fmt::Debug for MethodRouter { .field("put", &self.put) .field("trace", &self.trace) .field("connect", &self.connect) + .field("query", &self.query) .field("fallback", &self.fallback) .field("allow_header", &self.allow_header) .finish() @@ -657,6 +661,7 @@ where chained_handler_fn!(post, POST); chained_handler_fn!(put, PUT); chained_handler_fn!(trace, TRACE); + chained_handler_fn!(query, QUERY); /// Add a fallback [`Handler`] to the router. pub fn fallback(mut self, handler: H) -> Self @@ -690,6 +695,7 @@ where put, trace, connect, + query, fallback, allow_header: _, } = self; @@ -708,6 +714,7 @@ where (put, MethodFilter::PUT), (trace, MethodFilter::TRACE), (connect, MethodFilter::CONNECT), + (query, MethodFilter::QUERY), ] .into_iter() .filter_map(|(ep, f)| ep.is_some().then_some(f)) @@ -820,6 +827,7 @@ where put: MethodEndpoint::None, trace: MethodEndpoint::None, connect: MethodEndpoint::None, + query: MethodEndpoint::None, allow_header: AllowHeader::None, fallback: Fallback::Default(fallback), } @@ -837,6 +845,7 @@ where put: self.put.with_state(&state), trace: self.trace.with_state(&state), connect: self.connect.with_state(&state), + query: self.query.with_state(&state), allow_header: self.allow_header, fallback: self.fallback.with_state(state), } @@ -995,6 +1004,16 @@ where &["CONNECT"], ); + set_endpoint( + "QUERY", + &mut self.query, + endpoint, + filter, + MethodFilter::QUERY, + &mut self.allow_header, + &["QUERY"], + ); + self } @@ -1007,6 +1026,7 @@ where chained_service_fn!(post_service, POST); chained_service_fn!(put_service, PUT); chained_service_fn!(trace_service, TRACE); + chained_service_fn!(query_service, QUERY); #[doc = include_str!("../docs/method_routing/fallback.md")] pub fn fallback_service(mut self, svc: T) -> Self @@ -1043,6 +1063,7 @@ where put: self.put.map(layer_fn.clone()), trace: self.trace.map(layer_fn.clone()), connect: self.connect.map(layer_fn.clone()), + query: self.query.map(layer_fn.clone()), fallback: self.fallback.map(layer_fn), allow_header: self.allow_header, } @@ -1068,6 +1089,7 @@ where && self.put.is_none() && self.trace.is_none() && self.connect.is_none() + && self.query.is_none() { panic!( "Adding a route_layer before any routes is a no-op. \ @@ -1085,7 +1107,8 @@ where self.post = self.post.map(layer_fn.clone()); self.put = self.put.map(layer_fn.clone()); self.trace = self.trace.map(layer_fn.clone()); - self.connect = self.connect.map(layer_fn); + self.connect = self.connect.map(layer_fn.clone()); + self.query = self.query.map(layer_fn); self } @@ -1131,6 +1154,7 @@ where self.put = merge_inner(path, "PUT", self.put, other.put)?; self.trace = merge_inner(path, "TRACE", self.trace, other.trace)?; self.connect = merge_inner(path, "CONNECT", self.connect, other.connect)?; + self.query = merge_inner(path, "QUERY", self.query, other.query)?; self.fallback = self .fallback @@ -1206,6 +1230,7 @@ where put, trace, connect, + query, fallback, allow_header, } = self; @@ -1220,6 +1245,7 @@ where call!(req, DELETE, delete); call!(req, TRACE, trace); call!(req, CONNECT, connect); + call!(req, QUERY, query); let future = fallback.clone().call_with_state(req, state); @@ -1263,6 +1289,7 @@ impl Clone for MethodRouter { put: self.put.clone(), trace: self.trace.clone(), connect: self.connect.clone(), + query: self.query.clone(), fallback: self.fallback.clone(), allow_header: self.allow_header.clone(), } diff --git a/axum/src/routing/mod.rs b/axum/src/routing/mod.rs index a76eee5d..daf37102 100644 --- a/axum/src/routing/mod.rs +++ b/axum/src/routing/mod.rs @@ -45,7 +45,7 @@ pub use self::{into_make_service::IntoMakeService, method_filter::MethodFilter, pub use self::method_routing::{ any, any_service, connect, connect_service, delete, delete_service, get, get_service, head, head_service, on, on_service, options, options_service, patch, patch_service, post, - post_service, put, put_service, trace, trace_service, MethodRouter, + post_service, put, put_service, query, query_service, trace, trace_service, MethodRouter, }; macro_rules! panic_on_err { diff --git a/examples/Cargo.lock b/examples/Cargo.lock index d6658f79..6e2c06c3 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -2308,9 +2308,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa",