Skip to main content

actix_web/middleware/
normalize.rs

1//! For middleware documentation, see [`NormalizePath`].
2
3use actix_http::uri::{PathAndQuery, Uri};
4use actix_router::Url;
5use actix_service::{Service, Transform};
6use actix_utils::future::{ready, Ready};
7use bytes::Bytes;
8#[cfg(feature = "unicode")]
9use regex::Regex;
10#[cfg(not(feature = "unicode"))]
11use regex_lite::Regex;
12
13use crate::{
14    service::{ServiceRequest, ServiceResponse},
15    Error,
16};
17
18fn build_byte_index_map(old_path: &str, new_path: &str) -> Vec<u16> {
19    let old_path = old_path.as_bytes();
20    let new_path = new_path.as_bytes();
21
22    let mut map = Vec::with_capacity(old_path.len() + 1);
23    map.push(0);
24
25    let mut old_idx = 0usize;
26    let mut new_idx = 0usize;
27
28    while old_idx < old_path.len() {
29        if new_idx < new_path.len() && old_path[old_idx] == new_path[new_idx] {
30            new_idx += 1;
31        }
32
33        old_idx += 1;
34        map.push(new_idx.min(u16::MAX as usize) as u16);
35    }
36
37    map
38}
39
40/// Determines the behavior of the [`NormalizePath`] middleware.
41///
42/// The default is `TrailingSlash::Trim`.
43#[non_exhaustive]
44#[derive(Debug, Clone, Copy, Default)]
45pub enum TrailingSlash {
46    /// Trim trailing slashes from the end of the path.
47    ///
48    /// Using this will require all routes to omit trailing slashes for them to be accessible.
49    #[default]
50    Trim,
51
52    /// Only merge any present multiple trailing slashes.
53    ///
54    /// This option provides the best compatibility with behavior in actix-web v2.0.
55    MergeOnly,
56
57    /// Always add a trailing slash to the end of the path.
58    ///
59    /// Using this will require all routes have a trailing slash for them to be accessible.
60    Always,
61}
62
63/// Middleware for normalizing a request's path so that routes can be matched more flexibly.
64///
65/// # Normalization Steps
66/// - Merges consecutive slashes into one. (For example, `/path//one` always becomes `/path/one`.)
67/// - Appends a trailing slash if one is not present, removes one if present, or keeps trailing
68///   slashes as-is, depending on which [`TrailingSlash`] variant is supplied
69///   to [`new`](NormalizePath::new()).
70///
71/// # Default Behavior
72/// The default constructor chooses to strip trailing slashes from the end of paths with them
73/// ([`TrailingSlash::Trim`]). The implication is that route definitions should be defined without
74/// trailing slashes or else they will be inaccessible (or vice versa when using the
75/// `TrailingSlash::Always` behavior), as shown in the example tests below.
76///
77/// # Examples
78/// ```
79/// use actix_web::{web, middleware, App};
80///
81/// # actix_web::rt::System::new().block_on(async {
82/// let app = App::new()
83///     .wrap(middleware::NormalizePath::trim())
84///     .route("/test", web::get().to(|| async { "test" }))
85///     .route("/unmatchable/", web::get().to(|| async { "unmatchable" }));
86///
87/// use actix_web::http::StatusCode;
88/// use actix_web::test::{call_service, init_service, TestRequest};
89///
90/// let app = init_service(app).await;
91///
92/// let req = TestRequest::with_uri("/test").to_request();
93/// let res = call_service(&app, req).await;
94/// assert_eq!(res.status(), StatusCode::OK);
95///
96/// let req = TestRequest::with_uri("/test/").to_request();
97/// let res = call_service(&app, req).await;
98/// assert_eq!(res.status(), StatusCode::OK);
99///
100/// let req = TestRequest::with_uri("/unmatchable").to_request();
101/// let res = call_service(&app, req).await;
102/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
103///
104/// let req = TestRequest::with_uri("/unmatchable/").to_request();
105/// let res = call_service(&app, req).await;
106/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
107/// # })
108/// ```
109#[derive(Debug, Clone, Copy)]
110pub struct NormalizePath(TrailingSlash);
111
112impl Default for NormalizePath {
113    fn default() -> Self {
114        log::warn!(
115            "`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \
116            in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`."
117        );
118
119        Self(TrailingSlash::Trim)
120    }
121}
122
123impl NormalizePath {
124    /// Create new `NormalizePath` middleware with the specified trailing slash style.
125    pub fn new(trailing_slash_style: TrailingSlash) -> Self {
126        Self(trailing_slash_style)
127    }
128
129    /// Constructs a new `NormalizePath` middleware with [trim](TrailingSlash::Trim) semantics.
130    ///
131    /// Use this instead of `NormalizePath::default()` to avoid deprecation warning.
132    pub fn trim() -> Self {
133        Self::new(TrailingSlash::Trim)
134    }
135}
136
137impl<S, B> Transform<S, ServiceRequest> for NormalizePath
138where
139    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
140    S::Future: 'static,
141{
142    type Response = ServiceResponse<B>;
143    type Error = Error;
144    type Transform = NormalizePathNormalization<S>;
145    type InitError = ();
146    type Future = Ready<Result<Self::Transform, Self::InitError>>;
147
148    fn new_transform(&self, service: S) -> Self::Future {
149        ready(Ok(NormalizePathNormalization {
150            service,
151            merge_slash: Regex::new("//+").unwrap(),
152            trailing_slash_behavior: self.0,
153        }))
154    }
155}
156
157pub struct NormalizePathNormalization<S> {
158    service: S,
159    merge_slash: Regex,
160    trailing_slash_behavior: TrailingSlash,
161}
162
163impl<S, B> Service<ServiceRequest> for NormalizePathNormalization<S>
164where
165    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
166    S::Future: 'static,
167{
168    type Response = ServiceResponse<B>;
169    type Error = Error;
170    type Future = S::Future;
171
172    actix_service::forward_ready!(service);
173
174    fn call(&self, mut req: ServiceRequest) -> Self::Future {
175        let head = req.head_mut();
176
177        let original_path = head.uri.path();
178
179        // An empty path here means that the URI has no valid path. We skip normalization in this
180        // case, because adding a path can make the URI invalid
181        if !original_path.is_empty() {
182            // Either adds a string to the end (duplicates will be removed anyways) or trims all
183            // slashes from the end
184            let path = match self.trailing_slash_behavior {
185                TrailingSlash::Always => format!("{}/", original_path),
186                TrailingSlash::MergeOnly => original_path.to_string(),
187                TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(),
188            };
189
190            // normalize multiple /'s to one /
191            let path = self.merge_slash.replace_all(&path, "/");
192
193            // Ensure root paths are still resolvable. If resulting path is blank after previous
194            // step it means the path was one or more slashes. Reduce to single slash.
195            let path = if path.is_empty() { "/" } else { path.as_ref() };
196
197            // Check whether the path has been changed
198            //
199            // This check was previously implemented as string length comparison
200            //
201            // That approach fails when a trailing slash is added,
202            // and a duplicate slash is removed,
203            // since the length of the strings remains the same
204            //
205            // For example, the path "/v1//s" will be normalized to "/v1/s/"
206            // Both of the paths have the same length,
207            // so the change can not be deduced from the length comparison
208            if path != original_path {
209                let reindex = build_byte_index_map(original_path, path);
210                let mut parts = head.uri.clone().into_parts();
211                let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
212
213                let path = match query {
214                    Some(q) => Bytes::from(format!("{}?{}", path, q)),
215                    None => Bytes::copy_from_slice(path.as_bytes()),
216                };
217                parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
218
219                let uri = Uri::from_parts(parts).unwrap();
220                req.match_info_mut()
221                    .update_with_reindex(Url::new(uri.clone()), |idx| {
222                        let idx = usize::from(idx).min(reindex.len() - 1);
223                        reindex[idx]
224                    });
225                req.head_mut().uri = uri;
226            }
227        }
228        self.service.call(req)
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use actix_http::StatusCode;
235    use actix_service::IntoService;
236
237    use super::*;
238    use crate::{
239        guard::fn_guard,
240        test::{call_service, init_service, read_body, TestRequest},
241        web, App, HttpResponse,
242    };
243
244    #[actix_rt::test]
245    async fn test_wrap() {
246        let app = init_service(
247            App::new()
248                .wrap(NormalizePath::default())
249                .service(web::resource("/").to(HttpResponse::Ok))
250                .service(web::resource("/v1/something").to(HttpResponse::Ok))
251                .service(
252                    web::resource("/v2/something")
253                        .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
254                        .to(HttpResponse::Ok),
255                ),
256        )
257        .await;
258
259        let test_uris = vec![
260            "/",
261            "/?query=test",
262            "///",
263            "/v1//something",
264            "/v1//something////",
265            "//v1/something",
266            "//v1//////something",
267            "/v2//something?query=test",
268            "/v2//something////?query=test",
269            "//v2/something?query=test",
270            "//v2//////something?query=test",
271        ];
272
273        for uri in test_uris {
274            let req = TestRequest::with_uri(uri).to_request();
275            let res = call_service(&app, req).await;
276            assert!(res.status().is_success(), "Failed uri: {}", uri);
277        }
278    }
279
280    #[actix_rt::test]
281    async fn trim_trailing_slashes() {
282        let app = init_service(
283            App::new()
284                .wrap(NormalizePath(TrailingSlash::Trim))
285                .service(web::resource("/").to(HttpResponse::Ok))
286                .service(web::resource("/v1/something").to(HttpResponse::Ok))
287                .service(
288                    web::resource("/v2/something")
289                        .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
290                        .to(HttpResponse::Ok),
291                ),
292        )
293        .await;
294
295        let test_uris = vec![
296            "/",
297            "///",
298            "/v1/something",
299            "/v1/something/",
300            "/v1/something////",
301            "//v1//something",
302            "//v1//something//",
303            "/v2/something?query=test",
304            "/v2/something/?query=test",
305            "/v2/something////?query=test",
306            "//v2//something?query=test",
307            "//v2//something//?query=test",
308        ];
309
310        for uri in test_uris {
311            let req = TestRequest::with_uri(uri).to_request();
312            let res = call_service(&app, req).await;
313            assert!(res.status().is_success(), "Failed uri: {}", uri);
314        }
315    }
316
317    #[actix_rt::test]
318    async fn trim_root_trailing_slashes_with_query() {
319        let app = init_service(
320            App::new().wrap(NormalizePath(TrailingSlash::Trim)).service(
321                web::resource("/")
322                    .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
323                    .to(HttpResponse::Ok),
324            ),
325        )
326        .await;
327
328        let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"];
329
330        for uri in test_uris {
331            let req = TestRequest::with_uri(uri).to_request();
332            let res = call_service(&app, req).await;
333            assert!(res.status().is_success(), "Failed uri: {}", uri);
334        }
335    }
336
337    #[actix_rt::test]
338    async fn ensure_trailing_slash() {
339        let app = init_service(
340            App::new()
341                .wrap(NormalizePath(TrailingSlash::Always))
342                .service(web::resource("/").to(HttpResponse::Ok))
343                .service(web::resource("/v1/something/").to(HttpResponse::Ok))
344                .service(
345                    web::resource("/v2/something/")
346                        .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
347                        .to(HttpResponse::Ok),
348                ),
349        )
350        .await;
351
352        let test_uris = vec![
353            "/",
354            "///",
355            "/v1/something",
356            "/v1/something/",
357            "/v1/something////",
358            "//v1//something",
359            "//v1//something//",
360            "/v2/something?query=test",
361            "/v2/something/?query=test",
362            "/v2/something////?query=test",
363            "//v2//something?query=test",
364            "//v2//something//?query=test",
365        ];
366
367        for uri in test_uris {
368            let req = TestRequest::with_uri(uri).to_request();
369            let res = call_service(&app, req).await;
370            assert!(res.status().is_success(), "Failed uri: {}", uri);
371        }
372    }
373
374    #[actix_rt::test]
375    async fn ensure_root_trailing_slash_with_query() {
376        let app = init_service(
377            App::new()
378                .wrap(NormalizePath(TrailingSlash::Always))
379                .service(
380                    web::resource("/")
381                        .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
382                        .to(HttpResponse::Ok),
383                ),
384        )
385        .await;
386
387        let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"];
388
389        for uri in test_uris {
390            let req = TestRequest::with_uri(uri).to_request();
391            let res = call_service(&app, req).await;
392            assert!(res.status().is_success(), "Failed uri: {}", uri);
393        }
394    }
395
396    #[actix_rt::test]
397    async fn keep_trailing_slash_unchanged() {
398        let app = init_service(
399            App::new()
400                .wrap(NormalizePath(TrailingSlash::MergeOnly))
401                .service(web::resource("/").to(HttpResponse::Ok))
402                .service(web::resource("/v1/something").to(HttpResponse::Ok))
403                .service(web::resource("/v1/").to(HttpResponse::Ok))
404                .service(
405                    web::resource("/v2/something")
406                        .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
407                        .to(HttpResponse::Ok),
408                ),
409        )
410        .await;
411
412        let tests = vec![
413            ("/", true), // root paths should still work
414            ("/?query=test", true),
415            ("///", true),
416            ("/v1/something////", false),
417            ("/v1/something/", false),
418            ("//v1//something", true),
419            ("/v1/", true),
420            ("/v1", false),
421            ("/v1////", true),
422            ("//v1//", true),
423            ("///v1", false),
424            ("/v2/something?query=test", true),
425            ("/v2/something/?query=test", false),
426            ("/v2/something//?query=test", false),
427            ("//v2//something?query=test", true),
428        ];
429
430        for (uri, success) in tests {
431            let req = TestRequest::with_uri(uri).to_request();
432            let res = call_service(&app, req).await;
433            assert_eq!(res.status().is_success(), success, "Failed uri: {}", uri);
434        }
435    }
436
437    #[actix_rt::test]
438    async fn scope_dynamic_tail_path_is_reindexed() {
439        async fn handler(path: web::Path<String>) -> HttpResponse {
440            HttpResponse::Ok().body(path.into_inner())
441        }
442
443        let app = init_service(
444            App::new().service(
445                web::scope("{tail:.*}")
446                    .wrap(NormalizePath::trim())
447                    .default_service(web::to(handler)),
448            ),
449        )
450        .await;
451
452        let req = TestRequest::with_uri("/uaie//iuaei").to_request();
453        let res = call_service(&app, req).await;
454
455        assert_eq!(res.status(), StatusCode::OK);
456        assert_eq!(read_body(res).await, Bytes::from_static(b"uaie/iuaei"));
457    }
458
459    #[actix_rt::test]
460    async fn scope_static_prefix_skip_is_reindexed() {
461        let app = init_service(
462            App::new().service(
463                web::scope("/api")
464                    .wrap(NormalizePath::trim())
465                    .service(web::resource("/v1").to(HttpResponse::Ok)),
466            ),
467        )
468        .await;
469
470        let req = TestRequest::with_uri("/api//v1").to_request();
471        let res = call_service(&app, req).await;
472
473        assert_eq!(res.status(), StatusCode::OK);
474    }
475
476    #[actix_rt::test]
477    async fn no_path() {
478        let app = init_service(
479            App::new()
480                .wrap(NormalizePath::default())
481                .service(web::resource("/").to(HttpResponse::Ok)),
482        )
483        .await;
484
485        // This URI will be interpreted as an authority form, i.e. there is no path nor scheme
486        // (https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3)
487        let req = TestRequest::with_uri("eh").to_request();
488        let res = call_service(&app, req).await;
489        assert_eq!(res.status(), StatusCode::NOT_FOUND);
490    }
491
492    #[actix_rt::test]
493    async fn test_in_place_normalization() {
494        let srv = |req: ServiceRequest| {
495            assert_eq!("/v1/something", req.path());
496            ready(Ok(req.into_response(HttpResponse::Ok().finish())))
497        };
498
499        let normalize = NormalizePath::default()
500            .new_transform(srv.into_service())
501            .await
502            .unwrap();
503
504        let test_uris = vec![
505            "/v1//something////",
506            "///v1/something",
507            "//v1///something",
508            "/v1//something",
509        ];
510
511        for uri in test_uris {
512            let req = TestRequest::with_uri(uri).to_srv_request();
513            let res = normalize.call(req).await.unwrap();
514            assert!(res.status().is_success(), "Failed uri: {}", uri);
515        }
516    }
517
518    #[actix_rt::test]
519    async fn should_normalize_nothing() {
520        const URI: &str = "/v1/something";
521
522        let srv = |req: ServiceRequest| {
523            assert_eq!(URI, req.path());
524            ready(Ok(req.into_response(HttpResponse::Ok().finish())))
525        };
526
527        let normalize = NormalizePath::default()
528            .new_transform(srv.into_service())
529            .await
530            .unwrap();
531
532        let req = TestRequest::with_uri(URI).to_srv_request();
533        let res = normalize.call(req).await.unwrap();
534        assert!(res.status().is_success());
535    }
536
537    #[actix_rt::test]
538    async fn should_normalize_no_trail() {
539        let srv = |req: ServiceRequest| {
540            assert_eq!("/v1/something", req.path());
541            ready(Ok(req.into_response(HttpResponse::Ok().finish())))
542        };
543
544        let normalize = NormalizePath::default()
545            .new_transform(srv.into_service())
546            .await
547            .unwrap();
548
549        let req = TestRequest::with_uri("/v1/something/").to_srv_request();
550        let res = normalize.call(req).await.unwrap();
551        assert!(res.status().is_success());
552    }
553}