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.
3
+ //! Normalizes the request paths based on the provided `NormalizeMode`
5
4
//!
6
5
//! # Example
7
6
//!
8
7
//! ```
9
- //! use tower_http::normalize_path::NormalizePathLayer;
8
+ //! use tower_http::normalize_path::{ NormalizePathLayer, NormalizeMode} ;
10
9
//! use http::{Request, Response, StatusCode};
11
10
//! use http_body_util::Full;
12
11
//! use bytes::Bytes;
22
21
//!
23
22
//! let mut service = ServiceBuilder::new()
24
23
//! // trim trailing slashes from paths
25
- //! .layer(NormalizePathLayer::trim_trailing_slash( ))
24
+ //! .layer(NormalizePathLayer::new(NormalizeMode::Trim ))
26
25
//! .service_fn(handle);
27
26
//!
28
27
//! // call the service
@@ -45,27 +44,47 @@ use std::{
45
44
use tower_layer:: Layer ;
46
45
use tower_service:: Service ;
47
46
47
+ /// Different modes of normalizing paths
48
+ #[ derive( Debug , Copy , Clone ) ]
49
+ pub enum NormalizeMode {
50
+ /// Normalizes paths by trimming the trailing slashes, e.g. /foo/ -> /foo
51
+ Trim ,
52
+ /// Normalizes paths by appending trailing slash, e.g. /foo -> /foo/
53
+ Append ,
54
+ }
55
+
48
56
/// Layer that applies [`NormalizePath`] which normalizes paths.
49
57
///
50
58
/// See the [module docs](self) for more details.
51
59
#[ derive( Debug , Copy , Clone ) ]
52
- pub struct NormalizePathLayer { }
60
+ pub struct NormalizePathLayer {
61
+ mode : NormalizeMode ,
62
+ }
53
63
54
64
impl NormalizePathLayer {
55
65
/// Create a new [`NormalizePathLayer`].
56
66
///
57
67
/// Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
58
68
/// will be changed to `/foo` before reaching the inner service.
59
69
pub fn trim_trailing_slash ( ) -> Self {
60
- NormalizePathLayer { }
70
+ NormalizePathLayer {
71
+ mode : NormalizeMode :: Trim ,
72
+ }
73
+ }
74
+
75
+ /// Create a new [`NormalizePathLayer`].
76
+ ///
77
+ /// Creates a new `NormalizePathLayer` with the specified mode.
78
+ pub fn new ( mode : NormalizeMode ) -> Self {
79
+ NormalizePathLayer { mode }
61
80
}
62
81
}
63
82
64
83
impl < S > Layer < S > for NormalizePathLayer {
65
84
type Service = NormalizePath < S > ;
66
85
67
86
fn layer ( & self , inner : S ) -> Self :: Service {
68
- NormalizePath :: trim_trailing_slash ( inner)
87
+ NormalizePath :: new ( inner, self . mode )
69
88
}
70
89
}
71
90
@@ -74,16 +93,16 @@ impl<S> Layer<S> for NormalizePathLayer {
74
93
/// See the [module docs](self) for more details.
75
94
#[ derive( Debug , Copy , Clone ) ]
76
95
pub struct NormalizePath < S > {
96
+ mode : NormalizeMode ,
77
97
inner : S ,
78
98
}
79
99
80
100
impl < S > NormalizePath < S > {
81
101
/// Create a new [`NormalizePath`].
82
102
///
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.
85
- pub fn trim_trailing_slash ( inner : S ) -> Self {
86
- Self { inner }
103
+ /// Normalize path based on the specified `mode`
104
+ pub fn new ( inner : S , mode : NormalizeMode ) -> Self {
105
+ Self { mode, inner }
87
106
}
88
107
89
108
define_inner_service_accessors ! ( ) ;
@@ -103,12 +122,15 @@ where
103
122
}
104
123
105
124
fn call ( & mut self , mut req : Request < ReqBody > ) -> Self :: Future {
106
- normalize_trailing_slash ( req. uri_mut ( ) ) ;
125
+ match self . mode {
126
+ NormalizeMode :: Trim => trim_trailing_slash ( req. uri_mut ( ) ) ,
127
+ NormalizeMode :: Append => append_trailing_slash ( req. uri_mut ( ) ) ,
128
+ }
107
129
self . inner . call ( req)
108
130
}
109
131
}
110
132
111
- fn normalize_trailing_slash ( uri : & mut Uri ) {
133
+ fn trim_trailing_slash ( uri : & mut Uri ) {
112
134
if !uri. path ( ) . ends_with ( '/' ) && !uri. path ( ) . starts_with ( "//" ) {
113
135
return ;
114
136
}
@@ -137,14 +159,48 @@ fn normalize_trailing_slash(uri: &mut Uri) {
137
159
}
138
160
}
139
161
162
+ fn append_trailing_slash ( uri : & mut Uri ) {
163
+ if uri. path ( ) . ends_with ( "/" ) && !uri. path ( ) . ends_with ( "//" ) {
164
+ return ;
165
+ }
166
+
167
+ let trimmed = uri. path ( ) . trim_matches ( '/' ) ;
168
+ let new_path = if trimmed. is_empty ( ) {
169
+ "/" . to_string ( )
170
+ } else {
171
+ format ! ( "/{}/" , trimmed)
172
+ } ;
173
+
174
+ let mut parts = uri. clone ( ) . into_parts ( ) ;
175
+
176
+ let new_path_and_query = if let Some ( path_and_query) = & parts. path_and_query {
177
+ let new_path_and_query = if let Some ( query) = path_and_query. query ( ) {
178
+ Cow :: Owned ( format ! ( "{}?{}" , new_path, query) )
179
+ } else {
180
+ new_path. into ( )
181
+ }
182
+ . parse ( )
183
+ . unwrap ( ) ;
184
+
185
+ Some ( new_path_and_query)
186
+ } else {
187
+ Some ( new_path. parse ( ) . unwrap ( ) )
188
+ } ;
189
+
190
+ parts. path_and_query = new_path_and_query;
191
+ if let Ok ( new_uri) = Uri :: from_parts ( parts) {
192
+ * uri = new_uri;
193
+ }
194
+ }
195
+
140
196
#[ cfg( test) ]
141
197
mod tests {
142
198
use super :: * ;
143
199
use std:: convert:: Infallible ;
144
200
use tower:: { ServiceBuilder , ServiceExt } ;
145
201
146
202
#[ tokio:: test]
147
- async fn works ( ) {
203
+ async fn trim_works ( ) {
148
204
async fn handle ( request : Request < ( ) > ) -> Result < Response < String > , Infallible > {
149
205
Ok ( Response :: new ( request. uri ( ) . to_string ( ) ) )
150
206
}
@@ -168,63 +224,148 @@ mod tests {
168
224
#[ test]
169
225
fn is_noop_if_no_trailing_slash ( ) {
170
226
let mut uri = "/foo" . parse :: < Uri > ( ) . unwrap ( ) ;
171
- normalize_trailing_slash ( & mut uri) ;
227
+ trim_trailing_slash ( & mut uri) ;
172
228
assert_eq ! ( uri, "/foo" ) ;
173
229
}
174
230
175
231
#[ test]
176
232
fn maintains_query ( ) {
177
233
let mut uri = "/foo/?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
178
- normalize_trailing_slash ( & mut uri) ;
234
+ trim_trailing_slash ( & mut uri) ;
179
235
assert_eq ! ( uri, "/foo?a=a" ) ;
180
236
}
181
237
182
238
#[ test]
183
239
fn removes_multiple_trailing_slashes ( ) {
184
240
let mut uri = "/foo////" . parse :: < Uri > ( ) . unwrap ( ) ;
185
- normalize_trailing_slash ( & mut uri) ;
241
+ trim_trailing_slash ( & mut uri) ;
186
242
assert_eq ! ( uri, "/foo" ) ;
187
243
}
188
244
189
245
#[ test]
190
246
fn removes_multiple_trailing_slashes_even_with_query ( ) {
191
247
let mut uri = "/foo////?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
192
- normalize_trailing_slash ( & mut uri) ;
248
+ trim_trailing_slash ( & mut uri) ;
193
249
assert_eq ! ( uri, "/foo?a=a" ) ;
194
250
}
195
251
196
252
#[ test]
197
253
fn is_noop_on_index ( ) {
198
254
let mut uri = "/" . parse :: < Uri > ( ) . unwrap ( ) ;
199
- normalize_trailing_slash ( & mut uri) ;
255
+ trim_trailing_slash ( & mut uri) ;
200
256
assert_eq ! ( uri, "/" ) ;
201
257
}
202
258
203
259
#[ test]
204
260
fn removes_multiple_trailing_slashes_on_index ( ) {
205
261
let mut uri = "////" . parse :: < Uri > ( ) . unwrap ( ) ;
206
- normalize_trailing_slash ( & mut uri) ;
262
+ trim_trailing_slash ( & mut uri) ;
207
263
assert_eq ! ( uri, "/" ) ;
208
264
}
209
265
210
266
#[ test]
211
267
fn removes_multiple_trailing_slashes_on_index_even_with_query ( ) {
212
268
let mut uri = "////?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
213
- normalize_trailing_slash ( & mut uri) ;
269
+ trim_trailing_slash ( & mut uri) ;
214
270
assert_eq ! ( uri, "/?a=a" ) ;
215
271
}
216
272
217
273
#[ test]
218
274
fn removes_multiple_preceding_slashes_even_with_query ( ) {
219
275
let mut uri = "///foo//?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
220
- normalize_trailing_slash ( & mut uri) ;
276
+ trim_trailing_slash ( & mut uri) ;
221
277
assert_eq ! ( uri, "/foo?a=a" ) ;
222
278
}
223
279
224
280
#[ test]
225
281
fn removes_multiple_preceding_slashes ( ) {
226
282
let mut uri = "///foo" . parse :: < Uri > ( ) . unwrap ( ) ;
227
- normalize_trailing_slash ( & mut uri) ;
283
+ trim_trailing_slash ( & mut uri) ;
228
284
assert_eq ! ( uri, "/foo" ) ;
229
285
}
286
+
287
+ #[ tokio:: test]
288
+ async fn append_works ( ) {
289
+ async fn handle ( request : Request < ( ) > ) -> Result < Response < String > , Infallible > {
290
+ Ok ( Response :: new ( request. uri ( ) . to_string ( ) ) )
291
+ }
292
+
293
+ let mut svc = ServiceBuilder :: new ( )
294
+ . layer ( NormalizePathLayer :: new ( NormalizeMode :: Trim ) )
295
+ . service_fn ( handle) ;
296
+
297
+ let body = svc
298
+ . ready ( )
299
+ . await
300
+ . unwrap ( )
301
+ . call ( Request :: builder ( ) . uri ( "/foo" ) . body ( ( ) ) . unwrap ( ) )
302
+ . await
303
+ . unwrap ( )
304
+ . into_body ( ) ;
305
+
306
+ assert_eq ! ( body, "/foo/" ) ;
307
+ }
308
+
309
+ #[ test]
310
+ fn is_noop_if_trailing_slash ( ) {
311
+ let mut uri = "/foo/" . parse :: < Uri > ( ) . unwrap ( ) ;
312
+ append_trailing_slash ( & mut uri) ;
313
+ assert_eq ! ( uri, "/foo/" ) ;
314
+ }
315
+
316
+ #[ test]
317
+ fn append_maintains_query ( ) {
318
+ let mut uri = "/foo?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
319
+ append_trailing_slash ( & mut uri) ;
320
+ assert_eq ! ( uri, "/foo/?a=a" ) ;
321
+ }
322
+
323
+ #[ test]
324
+ fn append_only_keeps_one_slash ( ) {
325
+ let mut uri = "/foo////" . parse :: < Uri > ( ) . unwrap ( ) ;
326
+ append_trailing_slash ( & mut uri) ;
327
+ assert_eq ! ( uri, "/foo/" ) ;
328
+ }
329
+
330
+ #[ test]
331
+ fn append_only_keeps_one_slash_even_with_query ( ) {
332
+ let mut uri = "/foo////?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
333
+ append_trailing_slash ( & mut uri) ;
334
+ assert_eq ! ( uri, "/foo/?a=a" ) ;
335
+ }
336
+
337
+ #[ test]
338
+ fn append_is_noop_on_index ( ) {
339
+ let mut uri = "/" . parse :: < Uri > ( ) . unwrap ( ) ;
340
+ append_trailing_slash ( & mut uri) ;
341
+ assert_eq ! ( uri, "/" ) ;
342
+ }
343
+
344
+ #[ test]
345
+ fn append_removes_multiple_trailing_slashes_on_index ( ) {
346
+ let mut uri = "////" . parse :: < Uri > ( ) . unwrap ( ) ;
347
+ append_trailing_slash ( & mut uri) ;
348
+ assert_eq ! ( uri, "/" ) ;
349
+ }
350
+
351
+ #[ test]
352
+ fn append_removes_multiple_trailing_slashes_on_index_even_with_query ( ) {
353
+ let mut uri = "////?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
354
+ append_trailing_slash ( & mut uri) ;
355
+ assert_eq ! ( uri, "/?a=a" ) ;
356
+ }
357
+
358
+ #[ test]
359
+ fn append_removes_multiple_preceding_slashes_even_with_query ( ) {
360
+ let mut uri = "///foo//?a=a" . parse :: < Uri > ( ) . unwrap ( ) ;
361
+ append_trailing_slash ( & mut uri) ;
362
+ assert_eq ! ( uri, "/foo/?a=a" ) ;
363
+ }
364
+
365
+ #[ test]
366
+ fn append_removes_multiple_preceding_slashes ( ) {
367
+ let mut uri = "///foo" . parse :: < Uri > ( ) . unwrap ( ) ;
368
+ append_trailing_slash ( & mut uri) ;
369
+ assert_eq ! ( uri, "/foo/" ) ;
370
+ }
230
371
}
0 commit comments