From 273c760e1d7f321dd42e05ca81f3687948a26dce Mon Sep 17 00:00:00 2001 From: FarrosFR <56761912+farrosfr@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:13:10 +0700 Subject: [PATCH] feat(routing): support the HTTP QUERY method Add MethodFilter::QUERY and corresponding routing helpers query and query_service to allow handling requests using the standardized HTTP QUERY method (RFC 10008). Refs: #3799 --- axum-extra/src/routing/mod.rs | 23 ++++++++++++++ axum/src/routing/method_filter.rs | 8 +++++ axum/src/routing/method_routing.rs | 47 +++++++++++++++++++++++++++- axum/src/routing/mod.rs | 2 +- axum/src/routing/tests/mod.rs | 15 +++++++++ axum/src/test_helpers/test_client.rs | 9 ++++++ 6 files changed, 102 insertions(+), 2 deletions(-) diff --git a/axum-extra/src/routing/mod.rs b/axum-extra/src/routing/mod.rs index 3b3cdc6704..31d203bc35 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/src/routing/method_filter.rs b/axum/src/routing/method_filter.rs index 11d5c36de1..92129e26d4 100644 --- a/axum/src/routing/method_filter.rs +++ b/axum/src/routing/method_filter.rs @@ -43,6 +43,8 @@ impl MethodFilter { pub const PUT: Self = Self::from_bits(0b0_1000_0000); /// Match `TRACE` requests. pub const TRACE: Self = Self::from_bits(0b1_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), + other if other.as_str() == "QUERY" => Ok(Self::QUERY), other => Err(NoMatchingMethodFilter { method: other }), } } @@ -155,6 +158,11 @@ mod tests { MethodFilter::TRACE ); + assert_eq!( + MethodFilter::try_from(Method::from_bytes(b"QUERY").unwrap()).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 5c58291e69..f95aa903cb 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, } @@ -595,6 +598,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() @@ -640,6 +644,7 @@ where } chained_handler_fn!(connect, CONNECT); + chained_handler_fn!(query, QUERY); chained_handler_fn!(delete, DELETE); chained_handler_fn!(get, GET); chained_handler_fn!(head, HEAD); @@ -681,6 +686,7 @@ where put, trace, connect, + query, fallback, allow_header: _, } = self; @@ -699,6 +705,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)) @@ -811,6 +818,7 @@ where put: MethodEndpoint::None, trace: MethodEndpoint::None, connect: MethodEndpoint::None, + query: MethodEndpoint::None, allow_header: AllowHeader::None, fallback: Fallback::Default(fallback), } @@ -828,6 +836,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), } @@ -986,10 +995,21 @@ where &["CONNECT"], ); + set_endpoint( + "QUERY", + &mut self.query, + endpoint, + filter, + MethodFilter::QUERY, + &mut self.allow_header, + &["QUERY"], + ); + self } chained_service_fn!(connect_service, CONNECT); + chained_service_fn!(query_service, QUERY); chained_service_fn!(delete_service, DELETE); chained_service_fn!(get_service, GET); chained_service_fn!(head_service, HEAD); @@ -1034,6 +1054,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, } @@ -1059,6 +1080,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. \ @@ -1076,7 +1098,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 } @@ -1122,6 +1145,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 @@ -1166,6 +1190,24 @@ where pub(crate) fn call_with_state(&self, req: Request, state: S) -> RouteFuture { macro_rules! call { + ( + $req:expr, + QUERY, + $svc:expr + ) => { + if req.method().as_str() == "QUERY" { + match $svc { + MethodEndpoint::None => {} + MethodEndpoint::Route(route) => { + return route.clone().oneshot_inner_owned($req); + } + MethodEndpoint::BoxedHandler(handler) => { + let route = handler.clone().into_route(state); + return route.oneshot_inner_owned($req); + } + } + } + }; ( $req:expr, $method_variant:ident, @@ -1197,6 +1239,7 @@ where put, trace, connect, + query, fallback, allow_header, } = self; @@ -1211,6 +1254,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); @@ -1254,6 +1298,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 a76eee5d27..daf37102c0 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/axum/src/routing/tests/mod.rs b/axum/src/routing/tests/mod.rs index 20077a0b88..609227b8fb 100644 --- a/axum/src/routing/tests/mod.rs +++ b/axum/src/routing/tests/mod.rs @@ -1352,3 +1352,18 @@ async fn middleware_adding_body() { assert_eq!(res.text().await, "…"); } + +#[crate::test] +async fn query_method_routing() { + use crate::routing::query; + + let app = Router::new().route("/", query(|| async { "query handler" })); + + let client = TestClient::new(app); + + let query_method = Method::from_bytes(b"QUERY").unwrap(); + + let res = client.request(query_method, "/").await; + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.text().await, "query handler"); +} diff --git a/axum/src/test_helpers/test_client.rs b/axum/src/test_helpers/test_client.rs index c5e0c525df..a8dce0d309 100644 --- a/axum/src/test_helpers/test_client.rs +++ b/axum/src/test_helpers/test_client.rs @@ -80,6 +80,15 @@ impl TestClient { } } + #[allow(dead_code)] + pub fn request(&self, method: http::Method, url: &str) -> RequestBuilder { + RequestBuilder { + builder: self + .client + .request(method, format!("http://{}{url}", self.addr)), + } + } + #[allow(dead_code)] #[must_use] pub fn server_port(&self) -> u16 {