Skip to main content

rama_http/layer/
normalize_path.rs

1//! Middleware that normalizes paths.
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//! # Example
7//!
8//! ```
9//! use std::{iter::once, convert::Infallible};
10//! use rama_core::error::BoxError;
11//! use rama_core::service::service_fn;
12//! use rama_core::{Layer, Service};
13//! use rama_http::{Body, Request, Response, StatusCode};
14//! use rama_http::layer::normalize_path::NormalizePathLayer;
15//!
16//! # #[tokio::main]
17//! # async fn main() -> Result<(), BoxError> {
18//! async fn handle(req: Request) -> Result<Response, Infallible> {
19//!     // `req.uri().path()` will not have trailing slashes
20//!     # Ok(Response::new(Body::default()))
21//! }
22//!
23//! let mut service = (
24//!     // trim trailing slashes from paths
25//!     NormalizePathLayer::trim_trailing_slash(),
26//! ).into_layer(service_fn(handle));
27//!
28//! // call the service
29//! let request = Request::builder()
30//!     // `handle` will see `/foo`
31//!     .uri("/foo/")
32//!     .body(Body::default())?;
33//!
34//! service.serve(request).await?;
35//! #
36//! # Ok(())
37//! # }
38//! ```
39
40use crate::{Request, Response};
41use rama_core::{Layer, Service};
42use rama_utils::macros::define_inner_service_accessors;
43
44/// Different modes of normalizing paths
45#[derive(Debug, Copy, Clone)]
46enum NormalizeMode {
47    /// Normalizes paths by trimming the trailing slashes, e.g. /foo/ -> /foo
48    Trim,
49    /// Normalizes paths by appending trailing slash, e.g. /foo -> /foo/
50    Append,
51}
52
53/// Layer that applies [`NormalizePath`] which normalizes paths.
54///
55/// See the [module docs](self) for more details.
56#[derive(Debug, Clone)]
57pub struct NormalizePathLayer {
58    mode: NormalizeMode,
59}
60
61impl Default for NormalizePathLayer {
62    fn default() -> Self {
63        Self {
64            mode: NormalizeMode::Trim,
65        }
66    }
67}
68
69impl NormalizePathLayer {
70    /// Create a new [`NormalizePathLayer`].
71    ///
72    /// Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
73    /// will be changed to `/foo` before reaching the inner service.
74    #[must_use]
75    pub fn trim_trailing_slash() -> Self {
76        Self {
77            mode: NormalizeMode::Trim,
78        }
79    }
80
81    /// Create a new [`NormalizePathLayer`].
82    ///
83    /// Request paths without trailing slash will be appended with a trailing slash. For example, a request with `/foo`
84    /// will be changed to `/foo/` before reaching the inner service.
85    #[must_use]
86    pub fn append_trailing_slash() -> Self {
87        Self {
88            mode: NormalizeMode::Append,
89        }
90    }
91}
92
93impl<S> Layer<S> for NormalizePathLayer {
94    type Service = NormalizePath<S>;
95
96    fn layer(&self, inner: S) -> Self::Service {
97        NormalizePath {
98            mode: self.mode,
99            inner,
100        }
101    }
102}
103
104/// Middleware that normalizes paths.
105///
106/// See the [module docs](self) for more details.
107#[derive(Debug, Clone)]
108pub struct NormalizePath<S> {
109    mode: NormalizeMode,
110    inner: S,
111}
112
113impl<S> NormalizePath<S> {
114    /// Create a new [`NormalizePath`].
115    ///
116    /// Alias for [`Self::trim_trailing_slash`].
117    #[inline]
118    pub fn new(inner: S) -> Self {
119        Self::trim_trailing_slash(inner)
120    }
121
122    /// Create a new [`NormalizePath`].
123    ///
124    /// Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
125    /// will be changed to `/foo` before reaching the inner service.
126    pub fn trim_trailing_slash(inner: S) -> Self {
127        Self {
128            inner,
129            mode: NormalizeMode::Trim,
130        }
131    }
132
133    /// Create a new [`NormalizePath`].
134    ///
135    /// Request paths without trailing slash will be appended with a trailing slash. For example, a request with `/foo`
136    /// will be changed to `/foo/` before reaching the inner service.
137    pub fn append_trailing_slash(inner: S) -> Self {
138        Self {
139            inner,
140            mode: NormalizeMode::Append,
141        }
142    }
143
144    define_inner_service_accessors!();
145}
146
147impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for NormalizePath<S>
148where
149    S: Service<Request<ReqBody>, Output = Response<ResBody>>,
150    ReqBody: Send + 'static,
151    ResBody: Send + 'static,
152{
153    type Output = S::Output;
154    type Error = S::Error;
155
156    fn serve(
157        &self,
158        mut req: Request<ReqBody>,
159    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + '_ {
160        match self.mode {
161            NormalizeMode::Trim => {
162                req.uri_mut().path_mut().trim_trailing_slash();
163            }
164            NormalizeMode::Append => {
165                req.uri_mut().path_mut().append_trailing_slash();
166            }
167        }
168        self.inner.serve(req)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use rama_core::Layer;
176    use rama_core::service::service_fn;
177    use rama_net::uri::Uri;
178    use std::convert::Infallible;
179
180    #[tokio::test]
181    async fn works() {
182        async fn handle(request: Request<()>) -> Result<Response<String>, Infallible> {
183            Ok(Response::new(request.uri().to_string()))
184        }
185
186        let svc = NormalizePathLayer::trim_trailing_slash().into_layer(service_fn(handle));
187
188        let body = svc
189            .serve(Request::builder().uri("/foo/").body(()).unwrap())
190            .await
191            .unwrap()
192            .into_body();
193
194        assert_eq!(body, "/foo");
195    }
196
197    #[test]
198    fn is_noop_if_no_trailing_slash() {
199        let mut uri = "/foo".parse::<Uri>().unwrap();
200        uri.path_mut().trim_trailing_slash();
201        assert_eq!(uri.as_str(), "/foo");
202    }
203
204    #[test]
205    fn maintains_query() {
206        let mut uri = "/foo/?a=a".parse::<Uri>().unwrap();
207        uri.path_mut().trim_trailing_slash();
208        assert_eq!(uri.as_str(), "/foo?a=a");
209    }
210
211    #[test]
212    fn removes_multiple_trailing_slashes() {
213        let mut uri = "/foo////".parse::<Uri>().unwrap();
214        uri.path_mut().trim_trailing_slash();
215        assert_eq!(uri.as_str(), "/foo");
216    }
217
218    #[test]
219    fn removes_multiple_trailing_slashes_even_with_query() {
220        let mut uri = "/foo////?a=a".parse::<Uri>().unwrap();
221        uri.path_mut().trim_trailing_slash();
222        assert_eq!(uri.as_str(), "/foo?a=a");
223    }
224
225    #[test]
226    fn is_noop_on_index() {
227        let mut uri = "/".parse::<Uri>().unwrap();
228        uri.path_mut().trim_trailing_slash();
229        assert_eq!(uri.as_str(), "/");
230    }
231
232    #[test]
233    fn removes_multiple_trailing_slashes_on_index() {
234        let mut uri = "////".parse::<Uri>().unwrap();
235        uri.path_mut().trim_trailing_slash();
236        assert_eq!(uri.as_str(), "/");
237    }
238
239    #[test]
240    fn removes_multiple_trailing_slashes_on_index_even_with_query() {
241        let mut uri = "////?a=a".parse::<Uri>().unwrap();
242        uri.path_mut().trim_trailing_slash();
243        assert_eq!(uri.as_str(), "/?a=a");
244    }
245
246    #[test]
247    fn removes_multiple_preceding_slashes_even_with_query() {
248        let mut uri = "///foo//?a=a".parse::<Uri>().unwrap();
249        uri.path_mut().trim_trailing_slash();
250        assert_eq!(uri.as_str(), "/foo?a=a");
251    }
252
253    #[test]
254    fn removes_multiple_preceding_slashes() {
255        let mut uri = "///foo".parse::<Uri>().unwrap();
256        uri.path_mut().trim_trailing_slash();
257        assert_eq!(uri.as_str(), "/foo");
258    }
259
260    #[tokio::test]
261    async fn append_works() {
262        async fn handle(request: Request<()>) -> Result<Response<String>, Infallible> {
263            Ok(Response::new(request.uri().to_string()))
264        }
265
266        let svc = NormalizePathLayer::append_trailing_slash().into_layer(service_fn(handle));
267
268        let body = svc
269            .serve(Request::builder().uri("/foo").body(()).unwrap())
270            .await
271            .unwrap()
272            .into_body();
273
274        assert_eq!(body, "/foo/");
275    }
276
277    #[test]
278    fn is_noop_if_trailing_slash() {
279        let mut uri = "/foo/".parse::<Uri>().unwrap();
280        uri.path_mut().append_trailing_slash();
281        assert_eq!(uri.as_str(), "/foo/");
282    }
283
284    #[test]
285    fn append_maintains_query() {
286        let mut uri = "/foo?a=a".parse::<Uri>().unwrap();
287        uri.path_mut().append_trailing_slash();
288        assert_eq!(uri.as_str(), "/foo/?a=a");
289    }
290
291    #[test]
292    fn append_only_keeps_one_slash() {
293        let mut uri = "/foo////".parse::<Uri>().unwrap();
294        uri.path_mut().append_trailing_slash();
295        assert_eq!(uri.as_str(), "/foo/");
296    }
297
298    #[test]
299    fn append_only_keeps_one_slash_even_with_query() {
300        let mut uri = "/foo////?a=a".parse::<Uri>().unwrap();
301        uri.path_mut().append_trailing_slash();
302        assert_eq!(uri.as_str(), "/foo/?a=a");
303    }
304
305    #[test]
306    fn append_is_noop_on_index() {
307        let mut uri = "/".parse::<Uri>().unwrap();
308        uri.path_mut().append_trailing_slash();
309        assert_eq!(uri.as_str(), "/");
310    }
311
312    #[test]
313    fn append_removes_multiple_trailing_slashes_on_index() {
314        let mut uri = "////".parse::<Uri>().unwrap();
315        uri.path_mut().append_trailing_slash();
316        assert_eq!(uri.as_str(), "/");
317    }
318
319    #[test]
320    fn append_removes_multiple_trailing_slashes_on_index_even_with_query() {
321        let mut uri = "////?a=a".parse::<Uri>().unwrap();
322        uri.path_mut().append_trailing_slash();
323        assert_eq!(uri.as_str(), "/?a=a");
324    }
325
326    #[test]
327    fn append_removes_multiple_preceding_slashes_even_with_query() {
328        let mut uri = "///foo//?a=a".parse::<Uri>().unwrap();
329        uri.path_mut().append_trailing_slash();
330        assert_eq!(uri.as_str(), "/foo/?a=a");
331    }
332
333    #[test]
334    fn append_removes_multiple_preceding_slashes() {
335        let mut uri = "///foo".parse::<Uri>().unwrap();
336        uri.path_mut().append_trailing_slash();
337        assert_eq!(uri.as_str(), "/foo/");
338    }
339}