Skip to content

Commit 86615aa

Browse files
authored
normalize_path: Add Append mode (#547)
1 parent fa8848e commit 86615aa

File tree

3 files changed

+207
-23
lines changed

3 files changed

+207
-23
lines changed

tower-http/src/builder.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,16 @@ pub trait ServiceBuilderExt<L>: sealed::Sealed<L> + Sized {
366366
fn trim_trailing_slash(
367367
self,
368368
) -> ServiceBuilder<Stack<crate::normalize_path::NormalizePathLayer, L>>;
369+
370+
/// Append trailing slash to paths.
371+
///
372+
/// See [`tower_http::normalize_path`] for more details.
373+
///
374+
/// [`tower_http::normalize_path`]: crate::normalize_path
375+
#[cfg(feature = "normalize-path")]
376+
fn append_trailing_slash(
377+
self,
378+
) -> ServiceBuilder<Stack<crate::normalize_path::NormalizePathLayer, L>>;
369379
}
370380

371381
impl<L> sealed::Sealed<L> for ServiceBuilder<L> {}
@@ -596,4 +606,11 @@ impl<L> ServiceBuilderExt<L> for ServiceBuilder<L> {
596606
) -> ServiceBuilder<Stack<crate::normalize_path::NormalizePathLayer, L>> {
597607
self.layer(crate::normalize_path::NormalizePathLayer::trim_trailing_slash())
598608
}
609+
610+
#[cfg(feature = "normalize-path")]
611+
fn append_trailing_slash(
612+
self,
613+
) -> ServiceBuilder<Stack<crate::normalize_path::NormalizePathLayer, L>> {
614+
self.layer(crate::normalize_path::NormalizePathLayer::append_trailing_slash())
615+
}
599616
}

tower-http/src/normalize_path.rs

Lines changed: 177 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
//! Middleware that normalizes paths.
22
//!
3-
//! Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
4-
//! will be changed to `/foo` before reaching the inner service.
5-
//!
63
//! # Example
74
//!
85
//! ```
@@ -45,27 +42,53 @@ use std::{
4542
use tower_layer::Layer;
4643
use tower_service::Service;
4744

45+
/// Different modes of normalizing paths
46+
#[derive(Debug, Copy, Clone)]
47+
enum NormalizeMode {
48+
/// Normalizes paths by trimming the trailing slashes, e.g. /foo/ -> /foo
49+
Trim,
50+
/// Normalizes paths by appending trailing slash, e.g. /foo -> /foo/
51+
Append,
52+
}
53+
4854
/// Layer that applies [`NormalizePath`] which normalizes paths.
4955
///
5056
/// See the [module docs](self) for more details.
5157
#[derive(Debug, Copy, Clone)]
52-
pub struct NormalizePathLayer {}
58+
pub struct NormalizePathLayer {
59+
mode: NormalizeMode,
60+
}
5361

5462
impl NormalizePathLayer {
5563
/// Create a new [`NormalizePathLayer`].
5664
///
5765
/// Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
5866
/// will be changed to `/foo` before reaching the inner service.
5967
pub fn trim_trailing_slash() -> Self {
60-
NormalizePathLayer {}
68+
NormalizePathLayer {
69+
mode: NormalizeMode::Trim,
70+
}
71+
}
72+
73+
/// Create a new [`NormalizePathLayer`].
74+
///
75+
/// Request paths without trailing slash will be appended with a trailing slash. For example, a request with `/foo`
76+
/// will be changed to `/foo/` before reaching the inner service.
77+
pub fn append_trailing_slash() -> Self {
78+
NormalizePathLayer {
79+
mode: NormalizeMode::Append,
80+
}
6181
}
6282
}
6383

6484
impl<S> Layer<S> for NormalizePathLayer {
6585
type Service = NormalizePath<S>;
6686

6787
fn layer(&self, inner: S) -> Self::Service {
68-
NormalizePath::trim_trailing_slash(inner)
88+
NormalizePath {
89+
mode: self.mode,
90+
inner,
91+
}
6992
}
7093
}
7194

@@ -74,16 +97,25 @@ impl<S> Layer<S> for NormalizePathLayer {
7497
/// See the [module docs](self) for more details.
7598
#[derive(Debug, Copy, Clone)]
7699
pub struct NormalizePath<S> {
100+
mode: NormalizeMode,
77101
inner: S,
78102
}
79103

80104
impl<S> NormalizePath<S> {
81-
/// Create a new [`NormalizePath`].
82-
///
83-
/// Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
84-
/// will be changed to `/foo` before reaching the inner service.
105+
/// Construct a new [`NormalizePath`] with trim mode.
85106
pub fn trim_trailing_slash(inner: S) -> Self {
86-
Self { inner }
107+
Self {
108+
mode: NormalizeMode::Trim,
109+
inner,
110+
}
111+
}
112+
113+
/// Construct a new [`NormalizePath`] with append mode.
114+
pub fn append_trailing_slash(inner: S) -> Self {
115+
Self {
116+
mode: NormalizeMode::Append,
117+
inner,
118+
}
87119
}
88120

89121
define_inner_service_accessors!();
@@ -103,12 +135,15 @@ where
103135
}
104136

105137
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
106-
normalize_trailing_slash(req.uri_mut());
138+
match self.mode {
139+
NormalizeMode::Trim => trim_trailing_slash(req.uri_mut()),
140+
NormalizeMode::Append => append_trailing_slash(req.uri_mut()),
141+
}
107142
self.inner.call(req)
108143
}
109144
}
110145

111-
fn normalize_trailing_slash(uri: &mut Uri) {
146+
fn trim_trailing_slash(uri: &mut Uri) {
112147
if !uri.path().ends_with('/') && !uri.path().starts_with("//") {
113148
return;
114149
}
@@ -137,14 +172,48 @@ fn normalize_trailing_slash(uri: &mut Uri) {
137172
}
138173
}
139174

175+
fn append_trailing_slash(uri: &mut Uri) {
176+
if uri.path().ends_with("/") && !uri.path().ends_with("//") {
177+
return;
178+
}
179+
180+
let trimmed = uri.path().trim_matches('/');
181+
let new_path = if trimmed.is_empty() {
182+
"/".to_string()
183+
} else {
184+
format!("/{trimmed}/")
185+
};
186+
187+
let mut parts = uri.clone().into_parts();
188+
189+
let new_path_and_query = if let Some(path_and_query) = &parts.path_and_query {
190+
let new_path_and_query = if let Some(query) = path_and_query.query() {
191+
Cow::Owned(format!("{new_path}?{query}"))
192+
} else {
193+
new_path.into()
194+
}
195+
.parse()
196+
.unwrap();
197+
198+
Some(new_path_and_query)
199+
} else {
200+
Some(new_path.parse().unwrap())
201+
};
202+
203+
parts.path_and_query = new_path_and_query;
204+
if let Ok(new_uri) = Uri::from_parts(parts) {
205+
*uri = new_uri;
206+
}
207+
}
208+
140209
#[cfg(test)]
141210
mod tests {
142211
use super::*;
143212
use std::convert::Infallible;
144213
use tower::{ServiceBuilder, ServiceExt};
145214

146215
#[tokio::test]
147-
async fn works() {
216+
async fn trim_works() {
148217
async fn handle(request: Request<()>) -> Result<Response<String>, Infallible> {
149218
Ok(Response::new(request.uri().to_string()))
150219
}
@@ -168,63 +237,148 @@ mod tests {
168237
#[test]
169238
fn is_noop_if_no_trailing_slash() {
170239
let mut uri = "/foo".parse::<Uri>().unwrap();
171-
normalize_trailing_slash(&mut uri);
240+
trim_trailing_slash(&mut uri);
172241
assert_eq!(uri, "/foo");
173242
}
174243

175244
#[test]
176245
fn maintains_query() {
177246
let mut uri = "/foo/?a=a".parse::<Uri>().unwrap();
178-
normalize_trailing_slash(&mut uri);
247+
trim_trailing_slash(&mut uri);
179248
assert_eq!(uri, "/foo?a=a");
180249
}
181250

182251
#[test]
183252
fn removes_multiple_trailing_slashes() {
184253
let mut uri = "/foo////".parse::<Uri>().unwrap();
185-
normalize_trailing_slash(&mut uri);
254+
trim_trailing_slash(&mut uri);
186255
assert_eq!(uri, "/foo");
187256
}
188257

189258
#[test]
190259
fn removes_multiple_trailing_slashes_even_with_query() {
191260
let mut uri = "/foo////?a=a".parse::<Uri>().unwrap();
192-
normalize_trailing_slash(&mut uri);
261+
trim_trailing_slash(&mut uri);
193262
assert_eq!(uri, "/foo?a=a");
194263
}
195264

196265
#[test]
197266
fn is_noop_on_index() {
198267
let mut uri = "/".parse::<Uri>().unwrap();
199-
normalize_trailing_slash(&mut uri);
268+
trim_trailing_slash(&mut uri);
200269
assert_eq!(uri, "/");
201270
}
202271

203272
#[test]
204273
fn removes_multiple_trailing_slashes_on_index() {
205274
let mut uri = "////".parse::<Uri>().unwrap();
206-
normalize_trailing_slash(&mut uri);
275+
trim_trailing_slash(&mut uri);
207276
assert_eq!(uri, "/");
208277
}
209278

210279
#[test]
211280
fn removes_multiple_trailing_slashes_on_index_even_with_query() {
212281
let mut uri = "////?a=a".parse::<Uri>().unwrap();
213-
normalize_trailing_slash(&mut uri);
282+
trim_trailing_slash(&mut uri);
214283
assert_eq!(uri, "/?a=a");
215284
}
216285

217286
#[test]
218287
fn removes_multiple_preceding_slashes_even_with_query() {
219288
let mut uri = "///foo//?a=a".parse::<Uri>().unwrap();
220-
normalize_trailing_slash(&mut uri);
289+
trim_trailing_slash(&mut uri);
221290
assert_eq!(uri, "/foo?a=a");
222291
}
223292

224293
#[test]
225294
fn removes_multiple_preceding_slashes() {
226295
let mut uri = "///foo".parse::<Uri>().unwrap();
227-
normalize_trailing_slash(&mut uri);
296+
trim_trailing_slash(&mut uri);
228297
assert_eq!(uri, "/foo");
229298
}
299+
300+
#[tokio::test]
301+
async fn append_works() {
302+
async fn handle(request: Request<()>) -> Result<Response<String>, Infallible> {
303+
Ok(Response::new(request.uri().to_string()))
304+
}
305+
306+
let mut svc = ServiceBuilder::new()
307+
.layer(NormalizePathLayer::append_trailing_slash())
308+
.service_fn(handle);
309+
310+
let body = svc
311+
.ready()
312+
.await
313+
.unwrap()
314+
.call(Request::builder().uri("/foo").body(()).unwrap())
315+
.await
316+
.unwrap()
317+
.into_body();
318+
319+
assert_eq!(body, "/foo/");
320+
}
321+
322+
#[test]
323+
fn is_noop_if_trailing_slash() {
324+
let mut uri = "/foo/".parse::<Uri>().unwrap();
325+
append_trailing_slash(&mut uri);
326+
assert_eq!(uri, "/foo/");
327+
}
328+
329+
#[test]
330+
fn append_maintains_query() {
331+
let mut uri = "/foo?a=a".parse::<Uri>().unwrap();
332+
append_trailing_slash(&mut uri);
333+
assert_eq!(uri, "/foo/?a=a");
334+
}
335+
336+
#[test]
337+
fn append_only_keeps_one_slash() {
338+
let mut uri = "/foo////".parse::<Uri>().unwrap();
339+
append_trailing_slash(&mut uri);
340+
assert_eq!(uri, "/foo/");
341+
}
342+
343+
#[test]
344+
fn append_only_keeps_one_slash_even_with_query() {
345+
let mut uri = "/foo////?a=a".parse::<Uri>().unwrap();
346+
append_trailing_slash(&mut uri);
347+
assert_eq!(uri, "/foo/?a=a");
348+
}
349+
350+
#[test]
351+
fn append_is_noop_on_index() {
352+
let mut uri = "/".parse::<Uri>().unwrap();
353+
append_trailing_slash(&mut uri);
354+
assert_eq!(uri, "/");
355+
}
356+
357+
#[test]
358+
fn append_removes_multiple_trailing_slashes_on_index() {
359+
let mut uri = "////".parse::<Uri>().unwrap();
360+
append_trailing_slash(&mut uri);
361+
assert_eq!(uri, "/");
362+
}
363+
364+
#[test]
365+
fn append_removes_multiple_trailing_slashes_on_index_even_with_query() {
366+
let mut uri = "////?a=a".parse::<Uri>().unwrap();
367+
append_trailing_slash(&mut uri);
368+
assert_eq!(uri, "/?a=a");
369+
}
370+
371+
#[test]
372+
fn append_removes_multiple_preceding_slashes_even_with_query() {
373+
let mut uri = "///foo//?a=a".parse::<Uri>().unwrap();
374+
append_trailing_slash(&mut uri);
375+
assert_eq!(uri, "/foo/?a=a");
376+
}
377+
378+
#[test]
379+
fn append_removes_multiple_preceding_slashes() {
380+
let mut uri = "///foo".parse::<Uri>().unwrap();
381+
append_trailing_slash(&mut uri);
382+
assert_eq!(uri, "/foo/");
383+
}
230384
}

tower-http/src/service_ext.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,19 @@ pub trait ServiceExt {
413413
{
414414
crate::normalize_path::NormalizePath::trim_trailing_slash(self)
415415
}
416+
417+
/// Append trailing slash to paths.
418+
///
419+
/// See [`tower_http::normalize_path`] for more details.
420+
///
421+
/// [`tower_http::normalize_path`]: crate::normalize_path
422+
#[cfg(feature = "normalize-path")]
423+
fn append_trailing_slash(self) -> crate::normalize_path::NormalizePath<Self>
424+
where
425+
Self: Sized,
426+
{
427+
crate::normalize_path::NormalizePath::append_trailing_slash(self)
428+
}
416429
}
417430

418431
impl<T> ServiceExt for T {}

0 commit comments

Comments
 (0)