/@handle and /auth/callback shipped no cache directives at all, while / and every asset sent no-cache. Router::layer only wraps routes registered before it, so those two sat between the API's no-store layer and the static fallback's own headers and inherited neither. A shell with no directives and no validator is one a browser may cache heuristically, and this shell names the stylesheet and script of the build that produced it. A stale copy therefore runs old JavaScript against whatever the current deploy serves. Observed in production within minutes of the cutover: an old app.js drew its own markup into the new shell, never cleared the spinner the new shell starts with, and offered the old claim-handle placeholder — which was then submitted, creating an account nobody asked for.
- commit
- cb8d53952fb75bcb4a503f83344fd78406c364b0
- parent
- 8958a314101541f896a6f163a50246e1cc0c152b
- author
- Dami <dami.etoile@gmail.com>
| M | crates/ | 35 | +++++++++++++++++++++++++++++- |
@@ -169,12 +169,45 @@ fn static_assets(web_root: &std::path::Path, index: &std::path::Path) -> axum::r
169169 ///170170 /// Read per request rather than cached so editing the page during development does not171171 /// need a server restart.172+///173+/// The `no-cache` is load-bearing and has to be set here rather than by a layer.174+/// `Router::layer` only wraps routes registered before it, so these two sit175+/// between the API's `no-store` layer and the static fallback's own headers, and176+/// inherit neither. A shell with no directives and no validator is one a browser177+/// may cache heuristically — and this shell names the stylesheet and the script178+/// for the build that produced it, so a stale copy pins a visitor to assets that179+/// a later deploy has renamed or deleted. `/` never had the problem; `/@handle`180+/// silently did.172181 fn serve_index(path: PathBuf) -> impl Fn() -> std::future::Ready<Response> + Clone {173182 move || {174183 let body = std::fs::read_to_string(&path).unwrap_or_else(|_| {175184 "<!doctype html><p>web/index.html is missing.".to_string()176185 });177- std::future::ready(Html(body).into_response())186+ let mut response = Html(body).into_response();187+ response188+ .headers_mut()189+ .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));190+ std::future::ready(response)191+ }192+}193+194+195+mod tests {196+ use super::*;197+198+ 199+ fn the_spa_shell_is_never_cached_without_revalidating() {200+ let index = std::env::temp_dir().join(format!("devsite-index-{}.html", std::process::id()));201+ std::fs::write(&index, "<!doctype html><title>x</title>").unwrap();202+203+ let response = serve_index(index.clone())().into_inner();204+ assert_eq!(205+ response.headers().get(header::CACHE_CONTROL).unwrap(),206+ "no-cache",207+ "a cached shell pins a browser to assets a later deploy may have removed"208+ );209+210+ std::fs::remove_file(&index).ok();178211 }179212 }180213