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",