1
1
//! Middleware that normalizes paths.
2
2
//!
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
- //!
6
3
//! # Example
7
4
//!
8
5
//! ```
@@ -45,27 +42,53 @@ use std::{
45
42
use tower_layer:: Layer ;
46
43
use tower_service:: Service ;
47
44
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
+
48
54
/// Layer that applies [`NormalizePath`] which normalizes paths.
49
55
///
50
56
/// See the [module docs](self) for more details.
51
57
#[ derive( Debug , Copy , Clone ) ]
52
- pub struct NormalizePathLayer { }
58
+ pub struct NormalizePathLayer {
59
+ mode : NormalizeMode ,
60
+ }
53
61
54
62
impl NormalizePathLayer {
55
63
/// Create a new [`NormalizePathLayer`].
56
64
///
57
65
/// Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
58
66
/// will be changed to `/foo` before reaching the inner service.
59
67
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
+ }
61
81
}
62
82
}
63
83
64
84
impl < S > Layer < S > for NormalizePathLayer {
65
85
type Service = NormalizePath < S > ;
66
86
67
87
fn layer ( & self , inner : S ) -> Self :: Service {
68
- NormalizePath :: trim_trailing_slash ( inner)
88
+ NormalizePath {
89
+ mode : self . mode ,
90
+ inner,
91
+ }
69
92
}
70
93
}
71
94
@@ -74,16 +97,25 @@ impl<S> Layer<S> for NormalizePathLayer {
74
97
/// See the [module docs](self) for more details.
75
98
#[ derive( Debug , Copy , Clone ) ]
76
99
pub struct NormalizePath < S > {
100
+ mode : NormalizeMode ,
77
101
inner : S ,
78
102
}
79
103
80
104
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.
85
106
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
+ }
87
119
}
88
120
89
121
define_inner_service_accessors ! ( ) ;
@@ -103,12 +135,15 @@ where
103
135
}
104
136
105
137
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
+ }
107
142
self . inner . call ( req)
108
143
}
109
144
}
110
145
111
- fn normalize_trailing_slash ( uri : & mut Uri ) {
146
+ fn trim_trailing_slash ( uri : & mut Uri ) {
112
147
if !uri. path ( ) . ends_with ( '/' ) && !uri. path ( ) . starts_with ( "//" ) {
113
148
return ;
114
149
}
@@ -137,14 +172,48 @@ fn normalize_trailing_slash(uri: &mut Uri) {
137
172
}
138
173
}
139
174
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
+
140
209
#[ cfg( test) ]
141
210
mod tests {
142
211
use super :: * ;
143
212
use std:: convert:: Infallible ;
144
213
use tower:: { ServiceBuilder , ServiceExt } ;
145
214
146
215
#[ tokio:: test]
147
- async fn works ( ) {
216
+ async fn trim_works ( ) {
148
217
async fn handle ( request : Request < ( ) > ) -> Result < Response < String > , Infallible > {
149
218
Ok ( Response :: new ( request. uri ( ) . to_string ( ) ) )
150
219
}
@@ -168,63 +237,148 @@ mod tests {
168
237
#[ test]
169
238
fn is_noop_if_no_trailing_slash ( ) {
170
239
let mut uri = "/foo" . parse :: < Uri > ( ) . unwrap ( ) ;
171
- normalize_trailing_slash ( & mut uri) ;
240
+ trim_trailing_slash ( & mut uri) ;
172
241
assert_eq ! ( uri, "/foo" ) ;
173
242
}
174
243
175
244
#[ test]
176
245
fn maintains_query ( ) {
177
246
let mut uri = "/foo/?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
178
- normalize_trailing_slash ( & mut uri) ;
247
+ trim_trailing_slash ( & mut uri) ;
179
248
assert_eq ! ( uri, "/foo?a=a" ) ;
180
249
}
181
250
182
251
#[ test]
183
252
fn removes_multiple_trailing_slashes ( ) {
184
253
let mut uri = "/foo////" . parse :: < Uri > ( ) . unwrap ( ) ;
185
- normalize_trailing_slash ( & mut uri) ;
254
+ trim_trailing_slash ( & mut uri) ;
186
255
assert_eq ! ( uri, "/foo" ) ;
187
256
}
188
257
189
258
#[ test]
190
259
fn removes_multiple_trailing_slashes_even_with_query ( ) {
191
260
let mut uri = "/foo////?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
192
- normalize_trailing_slash ( & mut uri) ;
261
+ trim_trailing_slash ( & mut uri) ;
193
262
assert_eq ! ( uri, "/foo?a=a" ) ;
194
263
}
195
264
196
265
#[ test]
197
266
fn is_noop_on_index ( ) {
198
267
let mut uri = "/" . parse :: < Uri > ( ) . unwrap ( ) ;
199
- normalize_trailing_slash ( & mut uri) ;
268
+ trim_trailing_slash ( & mut uri) ;
200
269
assert_eq ! ( uri, "/" ) ;
201
270
}
202
271
203
272
#[ test]
204
273
fn removes_multiple_trailing_slashes_on_index ( ) {
205
274
let mut uri = "////" . parse :: < Uri > ( ) . unwrap ( ) ;
206
- normalize_trailing_slash ( & mut uri) ;
275
+ trim_trailing_slash ( & mut uri) ;
207
276
assert_eq ! ( uri, "/" ) ;
208
277
}
209
278
210
279
#[ test]
211
280
fn removes_multiple_trailing_slashes_on_index_even_with_query ( ) {
212
281
let mut uri = "////?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
213
- normalize_trailing_slash ( & mut uri) ;
282
+ trim_trailing_slash ( & mut uri) ;
214
283
assert_eq ! ( uri, "/?a=a" ) ;
215
284
}
216
285
217
286
#[ test]
218
287
fn removes_multiple_preceding_slashes_even_with_query ( ) {
219
288
let mut uri = "///foo//?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
220
- normalize_trailing_slash ( & mut uri) ;
289
+ trim_trailing_slash ( & mut uri) ;
221
290
assert_eq ! ( uri, "/foo?a=a" ) ;
222
291
}
223
292
224
293
#[ test]
225
294
fn removes_multiple_preceding_slashes ( ) {
226
295
let mut uri = "///foo" . parse :: < Uri > ( ) . unwrap ( ) ;
227
- normalize_trailing_slash ( & mut uri) ;
296
+ trim_trailing_slash ( & mut uri) ;
228
297
assert_eq ! ( uri, "/foo" ) ;
229
298
}
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
+ }
230
384
}
0 commit comments