1use actix_http::uri::{PathAndQuery, Uri};
4use actix_service::{Service, Transform};
5use actix_utils::future::{ready, Ready};
6use bytes::Bytes;
7#[cfg(feature = "unicode")]
8use regex::Regex;
9#[cfg(not(feature = "unicode"))]
10use regex_lite::Regex;
11
12use crate::{
13 service::{ServiceRequest, ServiceResponse},
14 Error,
15};
16
17#[non_exhaustive]
21#[derive(Debug, Clone, Copy, Default)]
22pub enum TrailingSlash {
23 #[default]
27 Trim,
28
29 MergeOnly,
33
34 Always,
38}
39
40#[derive(Debug, Clone, Copy)]
87pub struct NormalizePath(TrailingSlash);
88
89impl Default for NormalizePath {
90 fn default() -> Self {
91 log::warn!(
92 "`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \
93 in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`."
94 );
95
96 Self(TrailingSlash::Trim)
97 }
98}
99
100impl NormalizePath {
101 pub fn new(trailing_slash_style: TrailingSlash) -> Self {
103 Self(trailing_slash_style)
104 }
105
106 pub fn trim() -> Self {
110 Self::new(TrailingSlash::Trim)
111 }
112}
113
114impl<S, B> Transform<S, ServiceRequest> for NormalizePath
115where
116 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
117 S::Future: 'static,
118{
119 type Response = ServiceResponse<B>;
120 type Error = Error;
121 type Transform = NormalizePathNormalization<S>;
122 type InitError = ();
123 type Future = Ready<Result<Self::Transform, Self::InitError>>;
124
125 fn new_transform(&self, service: S) -> Self::Future {
126 ready(Ok(NormalizePathNormalization {
127 service,
128 merge_slash: Regex::new("//+").unwrap(),
129 trailing_slash_behavior: self.0,
130 }))
131 }
132}
133
134pub struct NormalizePathNormalization<S> {
135 service: S,
136 merge_slash: Regex,
137 trailing_slash_behavior: TrailingSlash,
138}
139
140impl<S, B> Service<ServiceRequest> for NormalizePathNormalization<S>
141where
142 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
143 S::Future: 'static,
144{
145 type Response = ServiceResponse<B>;
146 type Error = Error;
147 type Future = S::Future;
148
149 actix_service::forward_ready!(service);
150
151 fn call(&self, mut req: ServiceRequest) -> Self::Future {
152 let head = req.head_mut();
153
154 let original_path = head.uri.path();
155
156 if !original_path.is_empty() {
159 let path = match self.trailing_slash_behavior {
162 TrailingSlash::Always => format!("{}/", original_path),
163 TrailingSlash::MergeOnly => original_path.to_string(),
164 TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(),
165 };
166
167 let path = self.merge_slash.replace_all(&path, "/");
169
170 let path = if path.is_empty() { "/" } else { path.as_ref() };
173
174 if path != original_path {
186 let mut parts = head.uri.clone().into_parts();
187 let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
188
189 let path = match query {
190 Some(q) => Bytes::from(format!("{}?{}", path, q)),
191 None => Bytes::copy_from_slice(path.as_bytes()),
192 };
193 parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
194
195 let uri = Uri::from_parts(parts).unwrap();
196 req.match_info_mut().get_mut().update(&uri);
197 req.head_mut().uri = uri;
198 }
199 }
200 self.service.call(req)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use actix_http::StatusCode;
207 use actix_service::IntoService;
208
209 use super::*;
210 use crate::{
211 guard::fn_guard,
212 test::{call_service, init_service, TestRequest},
213 web, App, HttpResponse,
214 };
215
216 #[actix_rt::test]
217 async fn test_wrap() {
218 let app = init_service(
219 App::new()
220 .wrap(NormalizePath::default())
221 .service(web::resource("/").to(HttpResponse::Ok))
222 .service(web::resource("/v1/something").to(HttpResponse::Ok))
223 .service(
224 web::resource("/v2/something")
225 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
226 .to(HttpResponse::Ok),
227 ),
228 )
229 .await;
230
231 let test_uris = vec![
232 "/",
233 "/?query=test",
234 "///",
235 "/v1//something",
236 "/v1//something////",
237 "//v1/something",
238 "//v1//////something",
239 "/v2//something?query=test",
240 "/v2//something////?query=test",
241 "//v2/something?query=test",
242 "//v2//////something?query=test",
243 ];
244
245 for uri in test_uris {
246 let req = TestRequest::with_uri(uri).to_request();
247 let res = call_service(&app, req).await;
248 assert!(res.status().is_success(), "Failed uri: {}", uri);
249 }
250 }
251
252 #[actix_rt::test]
253 async fn trim_trailing_slashes() {
254 let app = init_service(
255 App::new()
256 .wrap(NormalizePath(TrailingSlash::Trim))
257 .service(web::resource("/").to(HttpResponse::Ok))
258 .service(web::resource("/v1/something").to(HttpResponse::Ok))
259 .service(
260 web::resource("/v2/something")
261 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
262 .to(HttpResponse::Ok),
263 ),
264 )
265 .await;
266
267 let test_uris = vec![
268 "/",
269 "///",
270 "/v1/something",
271 "/v1/something/",
272 "/v1/something////",
273 "//v1//something",
274 "//v1//something//",
275 "/v2/something?query=test",
276 "/v2/something/?query=test",
277 "/v2/something////?query=test",
278 "//v2//something?query=test",
279 "//v2//something//?query=test",
280 ];
281
282 for uri in test_uris {
283 let req = TestRequest::with_uri(uri).to_request();
284 let res = call_service(&app, req).await;
285 assert!(res.status().is_success(), "Failed uri: {}", uri);
286 }
287 }
288
289 #[actix_rt::test]
290 async fn trim_root_trailing_slashes_with_query() {
291 let app = init_service(
292 App::new().wrap(NormalizePath(TrailingSlash::Trim)).service(
293 web::resource("/")
294 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
295 .to(HttpResponse::Ok),
296 ),
297 )
298 .await;
299
300 let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"];
301
302 for uri in test_uris {
303 let req = TestRequest::with_uri(uri).to_request();
304 let res = call_service(&app, req).await;
305 assert!(res.status().is_success(), "Failed uri: {}", uri);
306 }
307 }
308
309 #[actix_rt::test]
310 async fn ensure_trailing_slash() {
311 let app = init_service(
312 App::new()
313 .wrap(NormalizePath(TrailingSlash::Always))
314 .service(web::resource("/").to(HttpResponse::Ok))
315 .service(web::resource("/v1/something/").to(HttpResponse::Ok))
316 .service(
317 web::resource("/v2/something/")
318 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
319 .to(HttpResponse::Ok),
320 ),
321 )
322 .await;
323
324 let test_uris = vec![
325 "/",
326 "///",
327 "/v1/something",
328 "/v1/something/",
329 "/v1/something////",
330 "//v1//something",
331 "//v1//something//",
332 "/v2/something?query=test",
333 "/v2/something/?query=test",
334 "/v2/something////?query=test",
335 "//v2//something?query=test",
336 "//v2//something//?query=test",
337 ];
338
339 for uri in test_uris {
340 let req = TestRequest::with_uri(uri).to_request();
341 let res = call_service(&app, req).await;
342 assert!(res.status().is_success(), "Failed uri: {}", uri);
343 }
344 }
345
346 #[actix_rt::test]
347 async fn ensure_root_trailing_slash_with_query() {
348 let app = init_service(
349 App::new()
350 .wrap(NormalizePath(TrailingSlash::Always))
351 .service(
352 web::resource("/")
353 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
354 .to(HttpResponse::Ok),
355 ),
356 )
357 .await;
358
359 let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"];
360
361 for uri in test_uris {
362 let req = TestRequest::with_uri(uri).to_request();
363 let res = call_service(&app, req).await;
364 assert!(res.status().is_success(), "Failed uri: {}", uri);
365 }
366 }
367
368 #[actix_rt::test]
369 async fn keep_trailing_slash_unchanged() {
370 let app = init_service(
371 App::new()
372 .wrap(NormalizePath(TrailingSlash::MergeOnly))
373 .service(web::resource("/").to(HttpResponse::Ok))
374 .service(web::resource("/v1/something").to(HttpResponse::Ok))
375 .service(web::resource("/v1/").to(HttpResponse::Ok))
376 .service(
377 web::resource("/v2/something")
378 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
379 .to(HttpResponse::Ok),
380 ),
381 )
382 .await;
383
384 let tests = vec![
385 ("/", true), ("/?query=test", true),
387 ("///", true),
388 ("/v1/something////", false),
389 ("/v1/something/", false),
390 ("//v1//something", true),
391 ("/v1/", true),
392 ("/v1", false),
393 ("/v1////", true),
394 ("//v1//", true),
395 ("///v1", false),
396 ("/v2/something?query=test", true),
397 ("/v2/something/?query=test", false),
398 ("/v2/something//?query=test", false),
399 ("//v2//something?query=test", true),
400 ];
401
402 for (uri, success) in tests {
403 let req = TestRequest::with_uri(uri).to_request();
404 let res = call_service(&app, req).await;
405 assert_eq!(res.status().is_success(), success, "Failed uri: {}", uri);
406 }
407 }
408
409 #[actix_rt::test]
410 async fn no_path() {
411 let app = init_service(
412 App::new()
413 .wrap(NormalizePath::default())
414 .service(web::resource("/").to(HttpResponse::Ok)),
415 )
416 .await;
417
418 let req = TestRequest::with_uri("eh").to_request();
421 let res = call_service(&app, req).await;
422 assert_eq!(res.status(), StatusCode::NOT_FOUND);
423 }
424
425 #[actix_rt::test]
426 async fn test_in_place_normalization() {
427 let srv = |req: ServiceRequest| {
428 assert_eq!("/v1/something", req.path());
429 ready(Ok(req.into_response(HttpResponse::Ok().finish())))
430 };
431
432 let normalize = NormalizePath::default()
433 .new_transform(srv.into_service())
434 .await
435 .unwrap();
436
437 let test_uris = vec![
438 "/v1//something////",
439 "///v1/something",
440 "//v1///something",
441 "/v1//something",
442 ];
443
444 for uri in test_uris {
445 let req = TestRequest::with_uri(uri).to_srv_request();
446 let res = normalize.call(req).await.unwrap();
447 assert!(res.status().is_success(), "Failed uri: {}", uri);
448 }
449 }
450
451 #[actix_rt::test]
452 async fn should_normalize_nothing() {
453 const URI: &str = "/v1/something";
454
455 let srv = |req: ServiceRequest| {
456 assert_eq!(URI, req.path());
457 ready(Ok(req.into_response(HttpResponse::Ok().finish())))
458 };
459
460 let normalize = NormalizePath::default()
461 .new_transform(srv.into_service())
462 .await
463 .unwrap();
464
465 let req = TestRequest::with_uri(URI).to_srv_request();
466 let res = normalize.call(req).await.unwrap();
467 assert!(res.status().is_success());
468 }
469
470 #[actix_rt::test]
471 async fn should_normalize_no_trail() {
472 let srv = |req: ServiceRequest| {
473 assert_eq!("/v1/something", req.path());
474 ready(Ok(req.into_response(HttpResponse::Ok().finish())))
475 };
476
477 let normalize = NormalizePath::default()
478 .new_transform(srv.into_service())
479 .await
480 .unwrap();
481
482 let req = TestRequest::with_uri("/v1/something/").to_srv_request();
483 let res = normalize.call(req).await.unwrap();
484 assert!(res.status().is_success());
485 }
486}