fix(web): never 304 a rewritten page — pages drop conditional validators, assets keep them

This commit is contained in:
2026-08-31 01:35:28 -04:00
parent 9518d9d5d1
commit c564e317ed
6 changed files with 742 additions and 81 deletions
+39 -6
View File
@@ -509,16 +509,23 @@ refresh** (the "the pages are too sticky" problem, phase 33). One
Starlette middleware (`app/core/caching.py`) applies the rule at the
transport layer:
- **HTML pages always revalidate.** Every page (`/`, `/sources.html`,
`/document.html`, `/login.html`, `/tuning.html`) ships
`Cache-Control: no-cache`, so each visit re-checks the page with the
server — a page never lingers in the browser's cache unchecked.
- **HTML pages always revalidate — and never 304.** Every page
(`/`, `/index.html`, `/sources.html`, `/document.html`, `/login.html`,
`/tuning.html`, `/git-sources.html`, `/history.html`, `/shared.html`,
plus the dynamic share page `/shared/<token>`) ships
`Cache-Control: no-cache`, **no `etag`, no `last-modified`**, so each
visit re-checks the page with the server and always gets a fresh 200
body — a page never lingers in the browser's cache unchecked, and a
revalidation can never be answered "not modified" (see *Cache busting
below* for why).
- **Assets are versioned and cached for a year.** The pages reference
their CSS/JS with a token (`/assets/styles.css?v=<token>`), and every
`/assets/*` response ships
`Cache-Control: public, max-age=31536000, immutable`. The token is what
identifies the content, so long-term caching is safe: a new token means
a new URL, which the browser fetches fresh.
a new URL, which the browser fetches fresh. A conditional `GET` on a
versioned asset URL may still be answered `304` — the URL already
encodes the version, so that is safe.
- **The token is the deploy.** In a git checkout (the normal case) it is
the short SHA of `HEAD` (`git rev-parse --short HEAD`), computed once
per process start — so **every commit/deploy flips the token** and the
@@ -531,7 +538,7 @@ transport layer:
endpoint's own `Cache-Control: no-cache` is set by the endpoint itself.
No CDN, no new services, no build-step change: the middleware rewrites
the asset references of the five known pages in flight. The unversioned
the asset references of the known pages in flight. The unversioned
asset paths keep working too (the static mount ignores the query string),
so old tabs and direct links to `/assets/…` still resolve.
@@ -540,6 +547,32 @@ so old tabs and direct links to `/assets/…` still resolve.
> requesting the versioned assets; every commit after that is picked up
> automatically.
### Cache busting: why pages never 304 (phase 54)
The `?v=<git short SHA>` token flips on the **next process start** — a
commit is a deploy, so the restarted server's HTML references new asset
URLs, and the browser fetches them fresh into its year-long asset cache.
- **`/assets/*`** is cached `immutable` for a year *under the versioned
URL* — a conditional `GET` may 304, because the URL already encodes
the version.
- **HTML pages** are served `no-cache` and **never 304**, publishing no
`etag` / `last-modified`. The reason: the page body the browser
receives is *rewritten per process* (its asset refs gain
`?v=<token>`), so the static file's upstream validators would describe
the *file*, not the *bytes served* — a conditional `GET` matching them
would 304 out of the rewrite and leave the browser on HTML pointing at
the **previous** commit's CSS/JS, which the year-long asset cache
serves until a hard reload (the hole measured in phase 54). Pages
therefore revalidate against the bytes actually served: always a full
200.
**Local development:** a `git` commit changes the token on the next
server restart. If a browser still shows an old layout after a restart,
hard-reload once (`Ctrl/Cmd-Shift-R`) — the phase-54 fix guarantees the
*next* navigation is current, but it cannot un-pin a document that a
pre-54 deploy already 304'd into the browser's cache.
## Checking retrieval quality
Ask the *real* pipeline (live aipi embeddings + the current KB) whether a