From a480d71f6d55cef84f273e5a5e0563d5354ab328 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sat, 16 May 2026 15:54:23 -0500 Subject: [PATCH] examples: fix `using_serve_dir_with_assets_fallback` to not expose assets at root (#3762) Co-authored-by: Claude Sonnet 4.6 --- examples/Cargo.lock | 1 + examples/static-file-server/Cargo.toml | 5 ++++- examples/static-file-server/src/main.rs | 17 ++++++++++---- examples/static-file-server/src/tests.rs | 28 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 examples/static-file-server/src/tests.rs diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 92f475ab..45eb3a5d 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -1611,6 +1611,7 @@ name = "example-static-file-server" version = "0.1.0" dependencies = [ "axum", + "http-body-util", "tokio", "tower", "tower-http", diff --git a/examples/static-file-server/Cargo.toml b/examples/static-file-server/Cargo.toml index 89418552..94fc935c 100644 --- a/examples/static-file-server/Cargo.toml +++ b/examples/static-file-server/Cargo.toml @@ -8,6 +8,9 @@ publish = false axum = { path = "../../axum" } tokio = { version = "1.0", features = ["full"] } tower = { version = "0.5.2", features = ["util"] } -tower-http = { version = "0.6.1", features = ["fs", "trace"] } +tower-http = { version = "0.6.1", features = ["fs", "set-status", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[dev-dependencies] +http-body-util = "0.1.0" diff --git a/examples/static-file-server/src/main.rs b/examples/static-file-server/src/main.rs index 8a244532..21ffa923 100644 --- a/examples/static-file-server/src/main.rs +++ b/examples/static-file-server/src/main.rs @@ -11,6 +11,7 @@ use std::net::SocketAddr; use tower::ServiceExt; use tower_http::{ services::{ServeDir, ServeFile}, + set_status::SetStatus, trace::TraceLayer, }; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -45,13 +46,18 @@ fn using_serve_dir() -> Router { fn using_serve_dir_with_assets_fallback() -> Router { // `ServeDir` allows setting a fallback if an asset is not found // so with this `GET /assets/doesnt-exist.jpg` will return `index.html` - // rather than a 404 - let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html")); + // rather than a 404. + // The `fallback_service` ensures that all other paths (the standard + // SPA pattern) also return `index.html`. We wrap it in `SetStatus` so + // the response uses 404 instead of 200 -- the path wasn't found, the SPA + // just handles routing client-side. + let index_html = SetStatus::new(ServeFile::new("assets/index.html"), StatusCode::NOT_FOUND); + let serve_dir = ServeDir::new("assets").not_found_service(index_html.clone()); Router::new() .route("/foo", get(|| async { "Hi from /foo" })) - .nest_service("/assets", serve_dir.clone()) - .fallback_service(serve_dir) + .nest_service("/assets", serve_dir) + .fallback_service(index_html) } fn using_serve_dir_only_from_root_via_fallback() -> Router { @@ -107,6 +113,9 @@ fn using_serve_file_from_a_route() -> Router { Router::new().route_service("/foo", ServeFile::new("assets/index.html")) } +#[cfg(test)] +mod tests; + async fn serve(app: Router, port: u16) { let addr = SocketAddr::from(([127, 0, 0, 1], port)); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); diff --git a/examples/static-file-server/src/tests.rs b/examples/static-file-server/src/tests.rs new file mode 100644 index 00000000..dbc0c474 --- /dev/null +++ b/examples/static-file-server/src/tests.rs @@ -0,0 +1,28 @@ +use super::using_serve_dir_with_assets_fallback; +use axum::{body::Body, http::Request, http::StatusCode}; +use http_body_util::BodyExt; +use tower::ServiceExt; + +const INDEX_HTML_CONTENT: &str = include_str!("../assets/index.html"); + +// `/script.js` at the root is not an asset under `/assets`, so it falls through to the SPA +// fallback. The fallback serves `index.html` with a 404 status -- the path wasn't found, but +// the SPA handles routing client-side. +#[tokio::test] +async fn assets_not_served_at_root() { + let app = using_serve_dir_with_assets_fallback(); + let response = app + .oneshot( + Request::builder() + .uri("/script.js") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert_eq!(body_str, INDEX_HTML_CONTENT); +}