Static serving: correct cache headers, no index fallback for assets
ci / docker (push) Successful in 13m11s
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
This commit is contained in:
Generated
+1
@@ -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
@@ -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
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user