From b7e37889932edcf521ca54e5ed30245f01180994 Mon Sep 17 00:00:00 2001 From: Zane Wang Date: Thu, 16 Jul 2026 02:16:22 -0700 Subject: [PATCH] fix(axum): deduplicate methods in the `Allow` header after merging `MethodRouter`s (#3836) --- axum/CHANGELOG.md | 3 +++ axum/src/routing/method_routing.rs | 30 +++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index ca9099fd..4327e8c8 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -26,6 +26,8 @@ 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]) +- **fixed:** `MethodRouter::merge` no longer lists a method twice in the `Allow` + header after merging `get` and `head` ([#3836]) [#3158]: https://github.com/tokio-rs/axum/pull/3158 [#3261]: https://github.com/tokio-rs/axum/pull/3261 @@ -38,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#3702]: https://github.com/tokio-rs/axum/pull/3702 [#3721]: https://github.com/tokio-rs/axum/pull/3721 [#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 # 0.8.9 diff --git a/axum/src/routing/method_routing.rs b/axum/src/routing/method_routing.rs index 5c58291e..59e080f8 100644 --- a/axum/src/routing/method_routing.rs +++ b/axum/src/routing/method_routing.rs @@ -574,10 +574,19 @@ impl AllowHeader { (Self::Skip, _) | (_, Self::Skip) => Self::Skip, (Self::None, Self::None) => Self::None, (Self::None, Self::Bytes(pick)) | (Self::Bytes(pick), Self::None) => Self::Bytes(pick), - (Self::Bytes(mut a), Self::Bytes(b)) => { - a.extend_from_slice(b","); - a.extend_from_slice(&b); - Self::Bytes(a) + (mut this @ Self::Bytes(_), Self::Bytes(b)) => { + match std::str::from_utf8(&b) { + Ok(methods) => { + for method in methods.split(',') { + append_allow_header(&mut this, method); + } + } + Err(_) => { + #[cfg(debug_assertions)] + panic!("`allow_header` contained invalid utf-8. This should never happen") + } + } + this } } } @@ -1222,7 +1231,7 @@ where } } -fn append_allow_header(allow_header: &mut AllowHeader, method: &'static str) { +fn append_allow_header(allow_header: &mut AllowHeader, method: &str) { match allow_header { AllowHeader::None => { *allow_header = AllowHeader::Bytes(BytesMut::from(method)); @@ -1545,6 +1554,17 @@ mod tests { assert_eq!(headers[ALLOW], "PUT,PATCH,GET,HEAD"); } + #[crate::test] + async fn allow_header_merging_get_into_head() { + let a = get(ok); + let b = head(created); + let mut svc = a.merge(b); + + let (status, headers, _) = call(Method::DELETE, &mut svc).await; + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + assert_eq!(headers[ALLOW], "GET,HEAD"); + } + #[crate::test] async fn allow_header_any() { let mut svc = any(ok);