From 002e51f36eecf13b464898a2f91c72a3adeb37f5 Mon Sep 17 00:00:00 2001 From: nils Date: Fri, 17 Jul 2026 17:30:13 +0200 Subject: [PATCH] Static serving: correct cache headers, no index fallback for assets - /assets/*: immutable one-year cache on hits, plain 404 on misses (previously a missing asset fell back to index.html served as its content type, breaking CSS/JS after every deploy for cached clients) - index.html and SPA routes: no-cache, so deploys are visible immediately --- Cargo.lock | 1 + Cargo.toml | 3 ++- src/bin/server.rs | 29 ++++++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ac1d15..226e48b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2574,6 +2574,7 @@ dependencies = [ "time", "tokio", "tokio-util", + "tower", "tower-http", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 34bc187..023518f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,8 @@ tempfile = "3" time = "0.3" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7", features = ["io"] } -tower-http = { version = "0.6", features = ["fs", "trace"] } +tower = { version = "0.5", features = ["util"] } +tower-http = { version = "0.6", features = ["fs", "set-header", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } urlencoding = "2" diff --git a/src/bin/server.rs b/src/bin/server.rs index 92442b3..9d3a9e1 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -1,4 +1,8 @@ +use axum::http::{header, HeaderValue}; +use tower::ServiceBuilder; +use tower_http::services::fs::ServeFileSystemResponseBody; use tower_http::services::{ServeDir, ServeFile}; +use tower_http::set_header::SetResponseHeaderLayer; use tower_http::trace::TraceLayer; use tracing_subscriber::EnvFilter; @@ -17,11 +21,30 @@ async fn main() -> anyhow::Result<()> { let state = AppState::new(config).await?; let static_dir = state.config.static_dir.clone(); - let index = std::path::Path::new(&static_dir).join("index.html"); - // .fallback (not .not_found_service) so SPA routes get index.html with a 200 - let spa = ServeDir::new(&static_dir).fallback(ServeFile::new(index)); + let static_root = std::path::Path::new(&static_dir); + // Hashed assets cache forever — except 404s, which would outlive the next deploy. + let assets = ServiceBuilder::new() + .map_response(|mut response: axum::http::Response| { + if response.status().is_success() { + response.headers_mut().insert( + header::CACHE_CONTROL, + HeaderValue::from_static("public, max-age=31536000, immutable"), + ); + } + response + }) + .service(ServeDir::new(static_root.join("assets"))); + // index.html revalidates every load (it names the asset hashes); + // .fallback (not .not_found_service) so SPA routes get it with a 200. + let spa = ServiceBuilder::new() + .layer(SetResponseHeaderLayer::overriding( + header::CACHE_CONTROL, + HeaderValue::from_static("no-cache"), + )) + .service(ServeDir::new(static_root).fallback(ServeFile::new(static_root.join("index.html")))); let app = photos::routes::router(&state) + .nest_service("/assets", assets) .fallback_service(spa) .layer(TraceLayer::new_for_http()) .with_state(state.clone());