Skip to content

Tag the UI assets so a rebuilt server changes what the reader sees - #75

Merged
aenawi merged 1 commit into
developfrom
revalidate-the-ui-assets
Sep 12, 2026
Merged

aenawi merged 1 commit into
developfrom
revalidate-the-ui-assets

Conversation

@aenawi

@aenawi aenawi commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Target

  • This is going into develop: the normal case for a fix, a chapter, a
    lab change, a typo
  • This is a release: develop → main, shipping to the published guide
  • This is a hotfix: straight into main, because published advice is
    currently locking readers out or leaving them exposed

What this changes

The embedded UI went out with no Cache-Control, no ETag and no
Last-Modified — embed.FS reports a zero modtime, so net/http had nothing to
put in Last-Modified either — and a browser handed a response with no freshness
information at all is free to invent its own. Rebuilding the control server
changed what the server had; it did not change what the reader saw. Now every
asset carries an ETag over its own bytes and Cache-Control: no-cache, so an
ordinary reload revalidates and a 304 keeps that cheap.

Closes #70

Kind

  • Prose: clarity, typo, dead link
  • The advice was wrong, unsafe, or out of date
  • Lab or sandbox: code, checks, a new scenario
  • A divergence between the lab and the sandbox, closed or newly documented
  • Tests
  • Something else

If this changes advice or a command

AGENTS.md and CLAUDE.md both told an agent that a hard reload was part of the
edit → rebuild → look loop. That was true and this change makes it untrue, so
both now say so. The rebuild step itself is unchanged and still required.

What it was measured against:

OS:          macOS 26.6.2 (Apple silicon)
Docker:      29.7.2 (compose v5.5.1)
Browser:     Chromium 147.0.7727.15 (headless, driven)
Go:          1.26.5
Measured on: 2026-09-12

What I ran, and what it printed.

Before, against the lab as it was running:

$ curl -sI http://localhost:8099/style.css
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 37646
Content-Type: text/css; charset=utf-8
Date: Sat, 12 Sep 2026 10:24:22 GMT

$ curl -sI http://localhost:8099/style.css | grep -iE 'cache|etag|last-modified|expires'
(no output)

After docker compose up -d --build --no-deps control:

$ for f in /style.css /app.js / /hud/scene.js; do curl -sI "http://localhost:8099$f" \
    | grep -iE 'HTTP|cache-control|etag'; done
HTTP/1.1 200 OK
Cache-Control: no-cache
Etag: "b075419be128ece483bbe3dca3480ada"
HTTP/1.1 200 OK
Cache-Control: no-cache
Etag: "ba66f80039c870bb5275d06f048fb169"
HTTP/1.1 200 OK
Cache-Control: no-cache
Etag: "310e01f0cfbe129dddda526d85331171"
HTTP/1.1 200 OK
Cache-Control: no-cache
Etag: "e4bb02ced962938fe1111f9be4abee8e"

Revalidation, and that nothing else moved:

$ curl -s -o /dev/null -w 'status=%{http_code} bytes=%{size_download}\n' \
    -H 'If-None-Match: "b075419be128ece483bbe3dca3480ada"' http://localhost:8099/style.css
status=304 bytes=0

$ curl -s -o /dev/null -w 'status=%{http_code} bytes=%{size_download}\n' \
    -H 'If-None-Match: "deadbeefdeadbeefdeadbeefdeadbeef"' http://localhost:8099/style.css
status=200 bytes=37646

$ curl -sI http://localhost:8099/nope.css | grep -iE 'HTTP|etag|cache-control'
HTTP/1.1 404 Not Found

$ curl -sI http://localhost:8099/api/state | grep -iE 'HTTP|etag|cache-control'
HTTP/1.1 200 OK

The bug itself, end to end, in a browser — which is the only place it was ever
visible. Load the page so Chromium caches style.css, append a marker rule to
lab/control/ui/style.css, rebuild control, then reload normally, no
Cmd+Shift+R:

ETag before the edit:  "b075419be128ece483bbe3dca3480ada"
ETag after the edit:   "9e11658a1eb6800c5796c41630ad86cd"

after a plain reload:
  getComputedStyle(document.documentElement)
    .getPropertyValue('--cache-probe')  ->  "reached-the-browser"

That is the reported bug gone. The other half — that revalidating on every load
stays cheap — from the PerformanceResourceTiming on a second plain reload with
nothing changed:

/style.css   transferSize 300   encodedBodySize 37692
/app.js      transferSize 300   encodedBodySize 0

300 bytes on the wire for a 37 KB stylesheet: the 304.

The marker rule was then reverted, control rebuilt again, style.css confirmed
byte-identical to develop (git diff empty) and its ETag confirmed back to
b075419be128ece483bbe3dca3480ada. The page reloads with no console errors.

  • I checked whether any other chapter makes the same claim — grep for
    "hard reload" and "caches" across the repo found only AGENTS.md and
    CLAUDE.md, both updated here. The guide's own pages are opened from
    file:// and are not served by this handler.
  • If this step can lock someone out of a machine, the warning sits before the step — n/a
  • The mechanism is explained, not just the command

Checks

  • make -C lab check passes
  • gofmt is clean (make check fails on any output at all)
  • New source files carry an SPDX-License-Identifier header

If this touches the guide's pages

Not touched. The handler serves the lab UI only; the guide still opens from
file:// with no server, no build step and no new dependency.

If this touches the lab or sandbox engine

The engine is not touched — this is the static file handler in front of it. The
/api/* routes are untouched and still answer with no cache headers, confirmed
above.

  • Nothing is drawn that was not measured
  • A failed attack does not render as a green tick
  • danger still outranks ok
  • Time is not faked
  • The engine still returns the rung that decided the outcome, not a bare pass/fail
  • If a score moved, the comparison table in lab/README.md moved with it — no score moved

Not in this change

Two things noticed while measuring, neither touched:

  • GET /index.html 301-redirects to /. That is http.FileServer's own
    behaviour and predates this change; a test notes it rather than asserting it
    away.
  • go vet-adjacent linters flag pre-existing issues in docker.go (a
    bufio.Scanner loop with no final sc.Err() check) and stream.go (string
    concatenation inside WriteString). Unrelated to this branch.

Licence

  • I have the right to submit this, and I am fine with it going out under
    CC BY-SA 4.0 (guide) or GPL-3.0-or-later (code), per the file I touched

style.css and app.js went out with no Cache-Control, no ETag and no
Last-Modified. embed.FS reports a zero modtime, so net/http had nothing
to put in Last-Modified either, and a browser handed a response with no
freshness information at all is free to invent its own.

That breaks the loop this lab is built around: edit, rebuild control,
look at the page. A rebuilt server that does not change the page reads
as a broken fix, and it cost a session on #68 debugging code that was
correct the whole time. It lands on readers too, not only editors —
anyone who opened the lab before pulling can be looking at a new app.js
against an old style.css with nothing indicating it.

Every embedded asset now carries an ETag over its own bytes and
Cache-Control: no-cache, which does not mean "do not store", it means
store it and ask first. The hash is computed once at startup because the
bytes are in the binary and cannot change while it runs. The header is
set before handing off to http.FileServer, which is not a trick:
http.ServeContent reads the ETag already on the ResponseWriter when it
evaluates If-None-Match, so 304 handling, ranges and content sniffing
all stay where they were.

No build step, no dependency, no version in index.html.

Measured against the running lab on 2026-09-12. Before, style.css
answered with no cache headers of any kind. After, it answers with
Cache-Control: no-cache and an ETag; a matching If-None-Match returns
304 with no body, a stale one returns the full 37646 bytes. Driving a
browser: with style.css already cached, editing it and rebuilding
control made a plain reload pick up the change, and a second reload with
nothing changed moved 300 bytes on the wire for a 37692-byte stylesheet.

AGENTS.md and CLAUDE.md both told agents a hard reload was part of the
loop. It no longer is, so they say so.

Closes #70
@aenawi
aenawi merged commit 49546e9 into develop Sep 12, 2026
1 check passed
@aenawi
aenawi deleted the revalidate-the-ui-assets branch September 12, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] The UI ships no cache headers and no version on its assets, so a rebuilt control server can still serve yesterday's CSS

1 participant