1 Commits
Author SHA1 Message Date
nils 002e51f36e Static serving: correct cache headers, no index fallback for assets
ci / docker (push) Successful in 13m11s
- /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
2026-07-17 17:30:13 +02:00
3 changed files with 29 additions and 4 deletions
Generated
+1
View File
@@ -2574,6 +2574,7 @@ dependencies = [
"time", "time",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tower",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
+2 -1
View File
@@ -37,7 +37,8 @@ tempfile = "3"
time = "0.3" time = "0.3"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["io"] } 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 = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
urlencoding = "2" urlencoding = "2"
+26 -3
View File
@@ -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::services::{ServeDir, ServeFile};
use tower_http::set_header::SetResponseHeaderLayer;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
@@ -17,11 +21,30 @@ async fn main() -> anyhow::Result<()> {
let state = AppState::new(config).await?; let state = AppState::new(config).await?;
let static_dir = state.config.static_dir.clone(); let static_dir = state.config.static_dir.clone();
let index = std::path::Path::new(&static_dir).join("index.html"); let static_root = std::path::Path::new(&static_dir);
// .fallback (not .not_found_service) so SPA routes get index.html with a 200 // Hashed assets cache forever — except 404s, which would outlive the next deploy.
let spa = ServeDir::new(&static_dir).fallback(ServeFile::new(index)); let assets = ServiceBuilder::new()
.map_response(|mut response: axum::http::Response<ServeFileSystemResponseBody>| {
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) let app = photos::routes::router(&state)
.nest_service("/assets", assets)
.fallback_service(spa) .fallback_service(spa)
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.with_state(state.clone()); .with_state(state.clone());