From 72be3355b8041ef2358fe7c36b6b65c7f508b5f6 Mon Sep 17 00:00:00 2001 From: daalfox Date: Sun, 2 Mar 2025 02:29:52 +0330 Subject: [PATCH 1/6] normalize_path: Add `Append` mode --- tower-http/src/builder.rs | 18 +++ tower-http/src/normalize_path.rs | 187 +++++++++++++++++++++++++++---- 2 files changed, 182 insertions(+), 23 deletions(-) diff --git a/tower-http/src/builder.rs b/tower-http/src/builder.rs index 58b789f2..cce1311a 100644 --- a/tower-http/src/builder.rs +++ b/tower-http/src/builder.rs @@ -364,6 +364,17 @@ pub trait ServiceBuilderExt: crate::sealed::Sealed + Sized { fn trim_trailing_slash( self, ) -> ServiceBuilder>; + + /// Normalize paths based on the specified `mode`. + /// + /// See [`tower_http::normalize_path`] for more details. + /// + /// [`tower_http::normalize_path`]: crate::normalize_path + #[cfg(feature = "normalize-path")] + fn normalize_path( + self, + mode: crate::normalize_path::NormalizeMode, + ) -> ServiceBuilder>; } impl crate::sealed::Sealed for ServiceBuilder {} @@ -594,4 +605,11 @@ impl ServiceBuilderExt for ServiceBuilder { ) -> ServiceBuilder> { self.layer(crate::normalize_path::NormalizePathLayer::trim_trailing_slash()) } + #[cfg(feature = "normalize-path")] + fn normalize_path( + self, + mode: crate::normalize_path::NormalizeMode, + ) -> ServiceBuilder> { + self.layer(crate::normalize_path::NormalizePathLayer::new(mode)) + } } diff --git a/tower-http/src/normalize_path.rs b/tower-http/src/normalize_path.rs index efc7be52..92e3fc70 100644 --- a/tower-http/src/normalize_path.rs +++ b/tower-http/src/normalize_path.rs @@ -1,12 +1,11 @@ //! Middleware that normalizes paths. //! -//! Any trailing slashes from request paths will be removed. For example, a request with `/foo/` -//! will be changed to `/foo` before reaching the inner service. +//! Normalizes the request paths based on the provided `NormalizeMode` //! //! # Example //! //! ``` -//! use tower_http::normalize_path::NormalizePathLayer; +//! use tower_http::normalize_path::{NormalizePathLayer, NormalizeMode}; //! use http::{Request, Response, StatusCode}; //! use http_body_util::Full; //! use bytes::Bytes; @@ -22,7 +21,7 @@ //! //! let mut service = ServiceBuilder::new() //! // trim trailing slashes from paths -//! .layer(NormalizePathLayer::trim_trailing_slash()) +//! .layer(NormalizePathLayer::new(NormalizeMode::Trim)) //! .service_fn(handle); //! //! // call the service @@ -45,11 +44,22 @@ use std::{ use tower_layer::Layer; use tower_service::Service; +/// Different modes of normalizing paths +#[derive(Debug, Copy, Clone)] +pub enum NormalizeMode { + /// Normalizes paths by trimming the trailing slashes, e.g. /foo/ -> /foo + Trim, + /// Normalizes paths by appending trailing slash, e.g. /foo -> /foo/ + Append, +} + /// Layer that applies [`NormalizePath`] which normalizes paths. /// /// See the [module docs](self) for more details. #[derive(Debug, Copy, Clone)] -pub struct NormalizePathLayer {} +pub struct NormalizePathLayer { + mode: NormalizeMode, +} impl NormalizePathLayer { /// Create a new [`NormalizePathLayer`]. @@ -57,7 +67,16 @@ impl NormalizePathLayer { /// Any trailing slashes from request paths will be removed. For example, a request with `/foo/` /// will be changed to `/foo` before reaching the inner service. pub fn trim_trailing_slash() -> Self { - NormalizePathLayer {} + NormalizePathLayer { + mode: NormalizeMode::Trim, + } + } + + /// Create a new [`NormalizePathLayer`]. + /// + /// Creates a new `NormalizePathLayer` with the specified mode. + pub fn new(mode: NormalizeMode) -> Self { + NormalizePathLayer { mode } } } @@ -65,7 +84,7 @@ impl Layer for NormalizePathLayer { type Service = NormalizePath; fn layer(&self, inner: S) -> Self::Service { - NormalizePath::trim_trailing_slash(inner) + NormalizePath::new(inner, self.mode) } } @@ -74,16 +93,16 @@ impl Layer for NormalizePathLayer { /// See the [module docs](self) for more details. #[derive(Debug, Copy, Clone)] pub struct NormalizePath { + mode: NormalizeMode, inner: S, } impl NormalizePath { /// Create a new [`NormalizePath`]. /// - /// Any trailing slashes from request paths will be removed. For example, a request with `/foo/` - /// will be changed to `/foo` before reaching the inner service. - pub fn trim_trailing_slash(inner: S) -> Self { - Self { inner } + /// Normalize path based on the specified `mode` + pub fn new(inner: S, mode: NormalizeMode) -> Self { + Self { mode, inner } } define_inner_service_accessors!(); @@ -103,12 +122,15 @@ where } fn call(&mut self, mut req: Request) -> Self::Future { - normalize_trailing_slash(req.uri_mut()); + match self.mode { + NormalizeMode::Trim => trim_trailing_slash(req.uri_mut()), + NormalizeMode::Append => append_trailing_slash(req.uri_mut()), + } self.inner.call(req) } } -fn normalize_trailing_slash(uri: &mut Uri) { +fn trim_trailing_slash(uri: &mut Uri) { if !uri.path().ends_with('/') && !uri.path().starts_with("//") { return; } @@ -137,6 +159,40 @@ fn normalize_trailing_slash(uri: &mut Uri) { } } +fn append_trailing_slash(uri: &mut Uri) { + if uri.path().ends_with("/") && !uri.path().ends_with("//") { + return; + } + + let trimmed = uri.path().trim_matches('/'); + let new_path = if trimmed.is_empty() { + "/".to_string() + } else { + format!("/{}/", trimmed) + }; + + let mut parts = uri.clone().into_parts(); + + let new_path_and_query = if let Some(path_and_query) = &parts.path_and_query { + let new_path_and_query = if let Some(query) = path_and_query.query() { + Cow::Owned(format!("{}?{}", new_path, query)) + } else { + new_path.into() + } + .parse() + .unwrap(); + + Some(new_path_and_query) + } else { + Some(new_path.parse().unwrap()) + }; + + parts.path_and_query = new_path_and_query; + if let Ok(new_uri) = Uri::from_parts(parts) { + *uri = new_uri; + } +} + #[cfg(test)] mod tests { use super::*; @@ -144,7 +200,7 @@ mod tests { use tower::{ServiceBuilder, ServiceExt}; #[tokio::test] - async fn works() { + async fn trim_works() { async fn handle(request: Request<()>) -> Result, Infallible> { Ok(Response::new(request.uri().to_string())) } @@ -168,63 +224,148 @@ mod tests { #[test] fn is_noop_if_no_trailing_slash() { let mut uri = "/foo".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/foo"); } #[test] fn maintains_query() { let mut uri = "/foo/?a=a".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/foo?a=a"); } #[test] fn removes_multiple_trailing_slashes() { let mut uri = "/foo////".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/foo"); } #[test] fn removes_multiple_trailing_slashes_even_with_query() { let mut uri = "/foo////?a=a".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/foo?a=a"); } #[test] fn is_noop_on_index() { let mut uri = "/".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/"); } #[test] fn removes_multiple_trailing_slashes_on_index() { let mut uri = "////".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/"); } #[test] fn removes_multiple_trailing_slashes_on_index_even_with_query() { let mut uri = "////?a=a".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/?a=a"); } #[test] fn removes_multiple_preceding_slashes_even_with_query() { let mut uri = "///foo//?a=a".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/foo?a=a"); } #[test] fn removes_multiple_preceding_slashes() { let mut uri = "///foo".parse::().unwrap(); - normalize_trailing_slash(&mut uri); + trim_trailing_slash(&mut uri); assert_eq!(uri, "/foo"); } + + #[tokio::test] + async fn append_works() { + async fn handle(request: Request<()>) -> Result, Infallible> { + Ok(Response::new(request.uri().to_string())) + } + + let mut svc = ServiceBuilder::new() + .layer(NormalizePathLayer::new(NormalizeMode::Trim)) + .service_fn(handle); + + let body = svc + .ready() + .await + .unwrap() + .call(Request::builder().uri("/foo").body(()).unwrap()) + .await + .unwrap() + .into_body(); + + assert_eq!(body, "/foo/"); + } + + #[test] + fn is_noop_if_trailing_slash() { + let mut uri = "/foo/".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/foo/"); + } + + #[test] + fn append_maintains_query() { + let mut uri = "/foo?a=a".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/foo/?a=a"); + } + + #[test] + fn append_only_keeps_one_slash() { + let mut uri = "/foo////".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/foo/"); + } + + #[test] + fn append_only_keeps_one_slash_even_with_query() { + let mut uri = "/foo////?a=a".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/foo/?a=a"); + } + + #[test] + fn append_is_noop_on_index() { + let mut uri = "/".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/"); + } + + #[test] + fn append_removes_multiple_trailing_slashes_on_index() { + let mut uri = "////".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/"); + } + + #[test] + fn append_removes_multiple_trailing_slashes_on_index_even_with_query() { + let mut uri = "////?a=a".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/?a=a"); + } + + #[test] + fn append_removes_multiple_preceding_slashes_even_with_query() { + let mut uri = "///foo//?a=a".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/foo/?a=a"); + } + + #[test] + fn append_removes_multiple_preceding_slashes() { + let mut uri = "///foo".parse::().unwrap(); + append_trailing_slash(&mut uri); + assert_eq!(uri, "/foo/"); + } } From cc21319dfac0f22d9fcb137ce6410b7e0636fc96 Mon Sep 17 00:00:00 2001 From: daalfox Date: Sun, 2 Mar 2025 02:46:14 +0330 Subject: [PATCH 2/6] normalize_path: Fix test --- tower-http/src/normalize_path.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tower-http/src/normalize_path.rs b/tower-http/src/normalize_path.rs index 92e3fc70..5fa50b4d 100644 --- a/tower-http/src/normalize_path.rs +++ b/tower-http/src/normalize_path.rs @@ -291,7 +291,7 @@ mod tests { } let mut svc = ServiceBuilder::new() - .layer(NormalizePathLayer::new(NormalizeMode::Trim)) + .layer(NormalizePathLayer::new(NormalizeMode::Append)) .service_fn(handle); let body = svc From db7bf31c1960ef09903d5eefc8bd13c010fa9a99 Mon Sep 17 00:00:00 2001 From: daalfox Date: Sun, 18 May 2025 10:39:40 +0330 Subject: [PATCH 3/6] Fix error --- tower-http/src/service_ext.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tower-http/src/service_ext.rs b/tower-http/src/service_ext.rs index 3221afab..72995b7d 100644 --- a/tower-http/src/service_ext.rs +++ b/tower-http/src/service_ext.rs @@ -411,7 +411,7 @@ pub trait ServiceExt { where Self: Sized, { - crate::normalize_path::NormalizePath::trim_trailing_slash(self) + crate::normalize_path::NormalizePath::new(self, crate::normalize_path::NormalizeMode::Trim) } } From 8035f8017e8ed83464b73925104193445214e979 Mon Sep 17 00:00:00 2001 From: daalfox Date: Sun, 18 May 2025 11:22:19 +0330 Subject: [PATCH 4/6] Make `NormalizeMode` private --- tower-http/src/builder.rs | 11 ++++---- tower-http/src/normalize_path.rs | 47 +++++++++++++++++++++++--------- tower-http/src/service_ext.rs | 15 +++++++++- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/tower-http/src/builder.rs b/tower-http/src/builder.rs index 13fe84bd..3bdcf64a 100644 --- a/tower-http/src/builder.rs +++ b/tower-http/src/builder.rs @@ -367,15 +367,14 @@ pub trait ServiceBuilderExt: sealed::Sealed + Sized { self, ) -> ServiceBuilder>; - /// Normalize paths based on the specified `mode`. + /// Append trailing slash to paths. /// /// See [`tower_http::normalize_path`] for more details. /// /// [`tower_http::normalize_path`]: crate::normalize_path #[cfg(feature = "normalize-path")] - fn normalize_path( + fn append_trailing_slash( self, - mode: crate::normalize_path::NormalizeMode, ) -> ServiceBuilder>; } @@ -607,11 +606,11 @@ impl ServiceBuilderExt for ServiceBuilder { ) -> ServiceBuilder> { self.layer(crate::normalize_path::NormalizePathLayer::trim_trailing_slash()) } + #[cfg(feature = "normalize-path")] - fn normalize_path( + fn append_trailing_slash( self, - mode: crate::normalize_path::NormalizeMode, ) -> ServiceBuilder> { - self.layer(crate::normalize_path::NormalizePathLayer::new(mode)) + self.layer(crate::normalize_path::NormalizePathLayer::append_trailing_slash()) } } diff --git a/tower-http/src/normalize_path.rs b/tower-http/src/normalize_path.rs index 5fa50b4d..512ba168 100644 --- a/tower-http/src/normalize_path.rs +++ b/tower-http/src/normalize_path.rs @@ -5,7 +5,7 @@ //! # Example //! //! ``` -//! use tower_http::normalize_path::{NormalizePathLayer, NormalizeMode}; +//! use tower_http::normalize_path::NormalizePathLayer; //! use http::{Request, Response, StatusCode}; //! use http_body_util::Full; //! use bytes::Bytes; @@ -21,7 +21,7 @@ //! //! let mut service = ServiceBuilder::new() //! // trim trailing slashes from paths -//! .layer(NormalizePathLayer::new(NormalizeMode::Trim)) +//! .layer(NormalizePathLayer::trim_trailing_slash()) //! .service_fn(handle); //! //! // call the service @@ -46,7 +46,7 @@ use tower_service::Service; /// Different modes of normalizing paths #[derive(Debug, Copy, Clone)] -pub enum NormalizeMode { +enum NormalizeMode { /// Normalizes paths by trimming the trailing slashes, e.g. /foo/ -> /foo Trim, /// Normalizes paths by appending trailing slash, e.g. /foo -> /foo/ @@ -74,9 +74,12 @@ impl NormalizePathLayer { /// Create a new [`NormalizePathLayer`]. /// - /// Creates a new `NormalizePathLayer` with the specified mode. - pub fn new(mode: NormalizeMode) -> Self { - NormalizePathLayer { mode } + /// Request paths without trailing slash will be appended with a trailing slash. For example, a request with `/foo` + /// will be changed to `/foo/` before reaching the inner service. + pub fn append_trailing_slash() -> Self { + NormalizePathLayer { + mode: NormalizeMode::Append, + } } } @@ -84,7 +87,16 @@ impl Layer for NormalizePathLayer { type Service = NormalizePath; fn layer(&self, inner: S) -> Self::Service { - NormalizePath::new(inner, self.mode) + match self.mode { + NormalizeMode::Trim => NormalizePath { + mode: NormalizeMode::Trim, + inner, + }, + NormalizeMode::Append => NormalizePath { + mode: NormalizeMode::Append, + inner, + }, + } } } @@ -98,11 +110,20 @@ pub struct NormalizePath { } impl NormalizePath { - /// Create a new [`NormalizePath`]. - /// - /// Normalize path based on the specified `mode` - pub fn new(inner: S, mode: NormalizeMode) -> Self { - Self { mode, inner } + /// Construct a new [`NormalizePath`] with trim mode. + pub fn trim(inner: S) -> Self { + Self { + mode: NormalizeMode::Trim, + inner, + } + } + + /// Construct a new [`NormalizePath`] with append mode. + pub fn append(inner: S) -> Self { + Self { + mode: NormalizeMode::Append, + inner, + } } define_inner_service_accessors!(); @@ -291,7 +312,7 @@ mod tests { } let mut svc = ServiceBuilder::new() - .layer(NormalizePathLayer::new(NormalizeMode::Append)) + .layer(NormalizePathLayer::append_trailing_slash()) .service_fn(handle); let body = svc diff --git a/tower-http/src/service_ext.rs b/tower-http/src/service_ext.rs index 72995b7d..e20cca62 100644 --- a/tower-http/src/service_ext.rs +++ b/tower-http/src/service_ext.rs @@ -411,7 +411,20 @@ pub trait ServiceExt { where Self: Sized, { - crate::normalize_path::NormalizePath::new(self, crate::normalize_path::NormalizeMode::Trim) + crate::normalize_path::NormalizePath::trim(self) + } + + /// Append trailing slash to paths. + /// + /// See [`tower_http::normalize_path`] for more details. + /// + /// [`tower_http::normalize_path`]: crate::normalize_path + #[cfg(feature = "normalize-path")] + fn append_trailing_slash(self) -> crate::normalize_path::NormalizePath + where + Self: Sized, + { + crate::normalize_path::NormalizePath::append(self) } } From 93b8365230b37e07d5662e1463c6ed5ea4f85cfd Mon Sep 17 00:00:00 2001 From: daalfox Date: Sun, 18 May 2025 11:47:57 +0330 Subject: [PATCH 5/6] Fix issues --- tower-http/src/normalize_path.rs | 22 ++++++++-------------- tower-http/src/service_ext.rs | 4 ++-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/tower-http/src/normalize_path.rs b/tower-http/src/normalize_path.rs index 512ba168..56d2e39c 100644 --- a/tower-http/src/normalize_path.rs +++ b/tower-http/src/normalize_path.rs @@ -1,6 +1,6 @@ //! Middleware that normalizes paths. //! -//! Normalizes the request paths based on the provided `NormalizeMode` +//! Normalizes the request paths //! //! # Example //! @@ -87,15 +87,9 @@ impl Layer for NormalizePathLayer { type Service = NormalizePath; fn layer(&self, inner: S) -> Self::Service { - match self.mode { - NormalizeMode::Trim => NormalizePath { - mode: NormalizeMode::Trim, - inner, - }, - NormalizeMode::Append => NormalizePath { - mode: NormalizeMode::Append, - inner, - }, + NormalizePath { + mode: self.mode, + inner, } } } @@ -111,7 +105,7 @@ pub struct NormalizePath { impl NormalizePath { /// Construct a new [`NormalizePath`] with trim mode. - pub fn trim(inner: S) -> Self { + pub fn trim_trailing_slash(inner: S) -> Self { Self { mode: NormalizeMode::Trim, inner, @@ -119,7 +113,7 @@ impl NormalizePath { } /// Construct a new [`NormalizePath`] with append mode. - pub fn append(inner: S) -> Self { + pub fn append_trailing_slash(inner: S) -> Self { Self { mode: NormalizeMode::Append, inner, @@ -189,14 +183,14 @@ fn append_trailing_slash(uri: &mut Uri) { let new_path = if trimmed.is_empty() { "/".to_string() } else { - format!("/{}/", trimmed) + format!("/{trimmed}/") }; let mut parts = uri.clone().into_parts(); let new_path_and_query = if let Some(path_and_query) = &parts.path_and_query { let new_path_and_query = if let Some(query) = path_and_query.query() { - Cow::Owned(format!("{}?{}", new_path, query)) + Cow::Owned(format!("{new_path}?{query}")) } else { new_path.into() } diff --git a/tower-http/src/service_ext.rs b/tower-http/src/service_ext.rs index e20cca62..8973d8a4 100644 --- a/tower-http/src/service_ext.rs +++ b/tower-http/src/service_ext.rs @@ -411,7 +411,7 @@ pub trait ServiceExt { where Self: Sized, { - crate::normalize_path::NormalizePath::trim(self) + crate::normalize_path::NormalizePath::trim_trailing_slash(self) } /// Append trailing slash to paths. @@ -424,7 +424,7 @@ pub trait ServiceExt { where Self: Sized, { - crate::normalize_path::NormalizePath::append(self) + crate::normalize_path::NormalizePath::append_trailing_slash(self) } } From 8b63dabeb0eef1184415755dd0569859356b6912 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sun, 18 May 2025 18:18:40 +0200 Subject: [PATCH 6/6] Update tower-http/src/normalize_path.rs --- tower-http/src/normalize_path.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tower-http/src/normalize_path.rs b/tower-http/src/normalize_path.rs index 56d2e39c..f9b9dd2e 100644 --- a/tower-http/src/normalize_path.rs +++ b/tower-http/src/normalize_path.rs @@ -1,7 +1,5 @@ //! Middleware that normalizes paths. //! -//! Normalizes the request paths -//! //! # Example //! //! ```