Repository: static-web-server/static-web-server (language: rust)

Issue:
Add trailing slash when missing
**Is your feature request related to a problem? Please describe.**
Current, managing relative links is a pain because navigating to `website.com/myfolder` returns the index page of that folder with no redirect. This means that if `index.html` references `./image.png`, the browser will try to get it from `website.com/image.png` instead of `website.com/myfolder/image.png`.

**Describe the solution you'd like**

When serving a folder, check if there is a trailing slash. If there isn't, do a redirect to a path with the slash at the end.

Add trailing slash when missing
**Is your feature request related to a problem? Please describe.**
Current, managing relative links is a pain because navigating to `website.com/myfolder` returns the index page of that folder with no redirect. This means that if `index.html` references `./image.png`, the browser will try to get it from `website.com/image.png` instead of `website.com/myfolder/image.png`.

**Describe the solution you'd like**

When serving a folder, check if there is a trailing slash. If there isn't, do a redirect to a path with the slash at the end.



Discussion / hints:
Yeah in fact we do this already for the directory listing so it should require a minor tweak to make it happen.
Yeah in fact we do this already for the directory listing so it should require a minor tweak to make it happen.

Fix patch:
diff --git a/src/static_files.rs b/src/static_files.rs
index dbae39ca..2433187f 100644
--- a/src/static_files.rs
+++ b/src/static_files.rs
@@ -58,30 +58,32 @@ pub async fn handle(
     let base = Arc::new(base_path.into());
     let (filepath, meta, auto_index) = path_from_tail(base, uri_path).await?;
 
+    // NOTE: `auto_index` appends an `index.html` to an `uri_path` of kind directory only.
+
+    // Check for a trailing slash on the current directory path
+    // and redirect if that path doesn't end with the slash char
+    if auto_index && !uri_path.ends_with('/') {
+        let uri = [uri_path, "/"].concat();
+        let loc = match HeaderValue::from_str(uri.as_str()) {
+            Ok(val) => val,
+            Err(err) => {
+                tracing::error!("invalid header value from current uri: {:?}", err);
+                return Err(StatusCode::INTERNAL_SERVER_ERROR);
+            }
+        };
+
+        let mut resp = Response::new(Body::empty());
+        resp.headers_mut().insert(hyper::header::LOCATION, loc);
+        *resp.status_mut() = StatusCode::PERMANENT_REDIRECT;
+        tracing::trace!("uri doesn't end with a slash so redirecting permanently");
+        return Ok(resp);
+    }
+
     // Directory listing
-    // 1. Check if "directory listing" feature is enabled,
+    // 1. Check if "directory listing" feature is enabled
     // if current path is a valid directory and
-    // if it does not contain an `index.html` file
+    // if it does not contain an `index.html` file (if a proper auto index is generated)
     if dir_listing && auto_index && !filepath.as_ref().exists() {
-        // Redirect if current path does not end with a slash char
-        if !uri_path.ends_with('/') {
-            let uri = [uri_path, "/"].concat();
-            let loc = match HeaderValue::from_str(uri.as_str()) {
-                Ok(val) => val,
-                Err(err) => {
-                    tracing::error!("invalid header value from current uri: {:?}", err);
-                    return Err(StatusCode::INTERNAL_SERVER_ERROR);
-                }
-            };
-
-            let mut resp = Response::new(Body::empty());
-            resp.headers_mut().insert(hyper::header::LOCATION, loc);
-            *resp.status_mut() = StatusCode::PERMANENT_REDIRECT;
-            tracing::trace!("uri doesn't end with a slash so redirect permanently");
-
-            return Ok(resp);
-        }
-
         return directory_listing(
             method,
             uri_path,


Test patch:
diff --git a/tests/static_files.rs b/tests/static_files.rs
index 9631cd15..9bc87355 100644
--- a/tests/static_files.rs
+++ b/tests/static_files.rs
@@ -115,6 +115,30 @@ mod tests {
         }
     }
 
+    #[tokio::test]
+    async fn handle_trailing_slash_redirection() {
+        let mut res = static_files::handle(
+            &Method::GET,
+            &HeaderMap::new(),
+            root_dir(),
+            "assets",
+            None,
+            false,
+            0,
+        )
+        .await
+        .expect("unexpected error response on `handle` function");
+
+        assert_eq!(res.status(), 308);
+        assert_eq!(res.headers()["location"], "assets/");
+
+        let body = hyper::body::to_bytes(res.body_mut())
+            .await
+            .expect("unexpected bytes error during `body` conversion");
+
+        assert_eq!(body, Bytes::new());
+    }
+
     #[tokio::test]
     async fn handle_append_index_on_dir() {
         let buf = fs::read(root_dir().join("index.html"))
@@ -134,9 +158,22 @@ mod tests {
                 )
                 .await
                 {
-                    Ok(res) => {
-                        assert_eq!(res.status(), 200);
-                        assert_eq!(res.headers()["content-length"], buf.len().to_string());
+                    Ok(mut res) => {
+                        if uri.is_empty() {
+                            // it should redirect permanently
+                            assert_eq!(res.status(), 308);
+                            assert_eq!(res.headers()["location"], "/");
+
+                            let body = hyper::body::to_bytes(res.body_mut())
+                                .await
+                                .expect("unexpected bytes error during `body` conversion");
+
+                            assert_eq!(body, Bytes::new());
+                        } else {
+                            // otherwise it should response with ok
+                            assert_eq!(res.status(), 200);
+                            assert_eq!(res.headers()["content-length"], buf.len().to_string());
+                        }
                     }
                     Err(_) => {
                         panic!("expected a status 200 but not a status error")