Skip to content

Commit a137ccd

Browse files
committed
fix: set Cache-Control: no-cache on SPA index.html
Railway's Fastly edge and Cloudflare both cache responses using the default max-age=14400 (4h) when no Cache-Control header is set. This caused stale index.html to be served after deploys, referencing old content-hashed assets that no longer exist → blank page until cache expires or manual purge. Set no-cache on index.html so CDNs always revalidate. Hashed assets in /dashboard/assets/ are still cacheable (immutable filenames).
1 parent 6f5322b commit a137ccd

File tree

2 files changed

+22
-1
lines changed

2 files changed

+22
-1
lines changed

src/edictum_server/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,13 @@ async def serve_spa(request: Request, full_path: str) -> FileResponse | HTMLResp
503503
"""Serve the React SPA — return index.html for all routes (client-side routing)."""
504504
index = _STATIC_DIR.resolve() / "index.html"
505505
if index.is_file():
506-
return FileResponse(index)
506+
return FileResponse(
507+
index,
508+
# Never cache index.html at CDN layer — it references content-hashed
509+
# assets that change on every build. Without this, CDNs (Railway's
510+
# Fastly edge, Cloudflare) cache stale HTML for hours.
511+
headers={"Cache-Control": "no-cache"},
512+
)
507513
return HTMLResponse(
508514
"<h1>Dashboard not built</h1>"
509515
"<p>Run <code>cd dashboard && pnpm build</code> or use the Vite dev server.</p>",

tests/test_spa_static_assets.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,18 @@ async def test_nonexistent_asset_not_served_as_html(no_auth_client: AsyncClient)
107107
)
108108
# 404 or 302 redirect — both acceptable. 200 text/html is the bug.
109109
assert resp.status_code != 200, "Missing asset returned 200 — catch-all is swallowing it"
110+
111+
112+
@pytest.mark.usefixtures("_static_dashboard")
113+
async def test_spa_index_has_no_cache_header(no_auth_client: AsyncClient) -> None:
114+
"""index.html must set Cache-Control: no-cache so CDNs don't cache stale HTML.
115+
116+
Without this, Railway (Fastly) and Cloudflare cache index.html for hours.
117+
When asset hashes change on redeploy, cached HTML references old assets → blank page.
118+
"""
119+
resp = await no_auth_client.get("/dashboard/")
120+
assert resp.status_code == 200
121+
cache_control = resp.headers.get("cache-control", "")
122+
assert (
123+
"no-cache" in cache_control
124+
), f"index.html has Cache-Control: {cache_control!r} — CDNs will cache stale HTML"

0 commit comments

Comments
 (0)