Serve front-end assets with Cache-Control: no-cache

Without an explicit directive ServeDir let browsers heuristically cache
the Flutter bundle, and the service worker kept serving stale assets, so
new icons/images did not appear after an upgrade (even on hard reload).
Force revalidation so users always get the latest assets; unchanged files
still return a cheap 304.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-07 18:15:52 -04:00
co-authored by Claude Opus 4.8
parent a12ff62fbd
commit 80f0f54785
2 changed files with 57 additions and 4 deletions
+2 -2
View File
@@ -21,11 +21,11 @@ lazy_static = "1.5.0"
duration-string = { version="0.5.3", features = ["serde"] }
clap = {version="4.5.54", features=["derive"]}
axum = "0.8.8"
tower-http = { version = "0.6.8", features = ["fs", "cors"] }
tower-http = { version = "0.6.8", features = ["fs", "cors", "set-header"] }
r2d2 = "0.8.10"
r2d2_sqlite = "0.32.0"
once_cell = "1.21.3"
tower = "0.5.3"
tower = { version = "0.5.3", features = ["util"] }
utoipa = { version = "5", features = ["axum_extras", "chrono"] }
utoipa-swagger-ui = { version = "9", features = ["axum"] }
simple-dns = "0.11.3"
+55 -2
View File
@@ -17,9 +17,10 @@ use axum::response::{Redirect, Response};
use axum::routing::{delete, get, post, put};
use axum::{Router, http};
use log::{debug, error, info};
use tower::ServiceBuilder;
use tower::{Layer, ServiceBuilder};
use tower_http::cors::{Any, CorsLayer};
use tower_http::services::ServeDir;
use tower_http::set_header::{SetResponseHeader, SetResponseHeaderLayer};
use utoipa::Modify;
use utoipa::OpenApi;
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
@@ -117,12 +118,28 @@ fn resolve_web_root(exe_dir: Option<&Path>) -> PathBuf {
}
}
/// Serve the bundled front-end assets with `Cache-Control: no-cache`.
///
/// Without an explicit directive the browser falls back to heuristic caching and
/// (together with the Flutter service worker) keeps serving stale assets after an
/// upgrade, so new images/icons never appear until the cache happens to expire.
/// `no-cache` does not disable caching: it forces a revalidation on every request,
/// which is a cheap `304 Not Modified` while files are unchanged and a fresh `200`
/// the moment they change. This guarantees users always see the latest assets.
fn web_asset_service(web_root: PathBuf) -> SetResponseHeader<ServeDir, http::HeaderValue> {
SetResponseHeaderLayer::overriding(
http::header::CACHE_CONTROL,
http::HeaderValue::from_static("no-cache"),
)
.layer(ServeDir::new(web_root))
}
pub async fn serve() -> Result<(), Box<dyn Error>> {
info!("Starting web server");
let exe = std::env::current_exe().ok();
let web_root = resolve_web_root(exe.as_deref().and_then(Path::parent));
info!("Serving front-end assets from {}", web_root.display());
let static_files = ServeDir::new(web_root);
let static_files = web_asset_service(web_root);
// Allow all origins and headers for API
let cors_layer = CorsLayer::new()
@@ -248,6 +265,42 @@ mod tests {
assert_eq!(resolve_web_root(None), PathBuf::from("./web"));
}
#[tokio::test]
async fn web_assets_are_served_with_no_cache() {
use axum::body::Body;
use tower::ServiceExt;
// A throwaway web root with a single asset to fetch back.
let web_root = std::env::temp_dir().join(format!(
"oott_web_asset_test_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&web_root).unwrap();
std::fs::write(web_root.join("asset.txt"), b"new-asset").unwrap();
let response = web_asset_service(web_root.clone())
.oneshot(
http::Request::builder()
.uri("/asset.txt")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
std::fs::remove_dir_all(&web_root).ok();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(http::header::CACHE_CONTROL)
.expect("front-end assets must carry a Cache-Control header"),
"no-cache"
);
}
#[test]
fn openapi_version_tracks_the_crate_version() {
// The OpenAPI spec must report the crate version (backend/Cargo.toml)