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 + '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 + '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 {
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 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 {