From ebf2c2f8e303b2d116d4c0cf5292fe97c1278a9e Mon Sep 17 00:00:00 2001 From: Park Jong Hyun <81476595+fhrmrk@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:28:10 +0900 Subject: [PATCH] fix(web): redirect a served root without a trailing slash (#987) `//` has no segments after the key, so the trailing-slash flag kept its initial value and the root's index.html was served at a URL without the slash; its relative links then resolved above the root and returned 404. The flag now comes from the path, so the root redirects to `///` like any other directory. Co-authored-by: fhrmrk <81476595+fhrmrk@users.noreply.github.com> --- crates/ide-web/src/server.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/ide-web/src/server.rs b/crates/ide-web/src/server.rs index cfb801930..c4c67cfc3 100644 --- a/crates/ide-web/src/server.rs +++ b/crates/ide-web/src/server.rs @@ -241,9 +241,9 @@ async fn resolve(shared: &Shared, target: &str) -> Response { let Some(root) = shared.roots.read().get(&key).cloned() else { return NOT_FOUND }; let Ok(real_root) = tokio::fs::canonicalize(&root).await else { return NOT_FOUND }; let mut file = root; - let mut trailing_slash = true; + // `//` names a directory too: it redirects like any other. + let trailing_slash = path.ends_with('/'); for seg in segments { - trailing_slash = seg.is_empty(); if seg.is_empty() { continue; } @@ -347,6 +347,10 @@ mod tests { let base = url.trim_end_matches("/web/index.html"); assert!(get(&format!("{base}/web/sub/a%20b.js"), None).await.contains("text/javascript")); assert!(get(&format!("{base}/web"), None).await.contains("Location: /")); + // The served root itself, so relative URLs of its index page resolve inside it. + let page = get(base, None).await; + let root_path = base.trim_start_matches("http://").split_once('/').unwrap().1; + assert!(page.starts_with("HTTP/1.1 301") && page.contains(&format!("Location: /{root_path}/\r\n")), "{page}"); assert!(get(&format!("{base}/web/"), None).await.ends_with("

hi

")); assert!(get(&format!("{base}/../secret.txt"), None).await.starts_with("HTTP/1.1 404")); assert!(get(&format!("{base}/%2e%2e/secret.txt"), None).await.starts_with("HTTP/1.1 404"));