Page is sent with Cache-Control: no-cache (closes #141)
check / check (push) Waiting to run

nginx sent index.html with no Cache-Control, so a browser could keep
it after a deploy and ask for hashed assets the new build no longer
has, and show an empty page. location / now adds Cache-Control:
no-cache, which covers its try_files fallback to /index.html, and
includes security-headers.conf again, since its own add_header drops
the server's. /assets/ and the locations passed to the backend are
unchanged.

script/frontend-viewport-test fails unless / and a path that falls
back to the page carry Cache-Control: no-cache and
Content-Security-Policy, which stands for the security headers.

Model: opus-5-5
This commit is contained in:
2026-10-18 23:50:38 +00:00
parent 007190b541
commit 33859b1dc1
4 changed files with 39 additions and 3 deletions
+6 -3
View File
@@ -86,8 +86,9 @@ either one is broken. We provide:
- `script/frontend-fmt-check` — check prettier formatting (read-only) - `script/frontend-fmt-check` — check prettier formatting (read-only)
- `script/frontend-viewport-test` — responsive-layout verification of the built - `script/frontend-viewport-test` — responsive-layout verification of the built
frontend in a containerised headless Chrome (see frontend in a containerised headless Chrome (see
[test/viewport/README.md](test/viewport/README.md)). Not part of [test/viewport/README.md](test/viewport/README.md)). It first checks that the
`script/check`: it takes minutes. page is served with `Cache-Control: no-cache` and the security headers. Not
part of `script/check`: it takes minutes.
- `script/docker` — build the image from `Dockerfile` without the build cache, - `script/docker` — build the image from `Dockerfile` without the build cache,
tagged `netwatch` via `script/projectname` tagged `netwatch` via `script/projectname`
- `script/cibuild` — CI entrypoint: runs `script/bootstrap` and `script/check`, - `script/cibuild` — CI entrypoint: runs `script/bootstrap` and `script/check`,
@@ -435,7 +436,9 @@ address nginx passes on and no other. The image:
each entry with `netwatch-server check-cidr`, which parses it as the backend each entry with `netwatch-server check-cidr`, which parses it as the backend
parses its own `TRUSTED_PROXIES` parses its own `TRUSTED_PROXIES`
- Sends access logs to stdout - Sends access logs to stdout
- Caches static assets with immutable headers - Sends the page with `Cache-Control: no-cache`, so the browser revalidates it
on every load; only the files under `/assets/`, whose names change with every
build, are cached as immutable
- Sends the security headers `REPO_POLICIES.md` requires on every response, as - Sends the security headers `REPO_POLICIES.md` requires on every response, as
`configs/security-headers.conf` sets them, in place of the backend's own `configs/security-headers.conf` sets them, in place of the backend's own
- Stores reports in `DATA_DIR`, `/data/reports` by default, on the `/data` - Stores reports in `DATA_DIR`, `/data/reports` by default, on the `/data`
+10
View File
@@ -21,6 +21,16 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of
# Completed Steps # Completed Steps
- 2026-10-18: the image's nginx sends the page with `Cache-Control: no-cache`
([#141](https://git.eeqj.de/sneak/netwatch/issues/141)), so the browser
revalidates it on every load. It went out with no `Cache-Control`, so a
browser could keep it after a deploy and ask for files under `/assets/` that
the new build no longer has, and show an empty page. A path with no file of
its own falls back to the page and gets the same header. That location now
includes the security headers again, as `/assets/` does, since its own
`add_header` drops the server's. `/assets/` keeps its immutable caching.
`make frontend-viewport-test` fails unless `/` and a path that falls back to
the page carry `Cache-Control: no-cache` and the security headers
- 2026-10-18: a browser that cannot format the header's clocks still probes - 2026-10-18: a browser that cannot format the header's clocks still probes
([#149](https://git.eeqj.de/sneak/netwatch/issues/149)). The clocks name the ([#149](https://git.eeqj.de/sneak/netwatch/issues/149)). The clocks name the
local time zone through `Intl.DateTimeFormat`'s `shortGeneric` option, which local time zone through `Intl.DateTimeFormat`'s `shortGeneric` option, which
+4
View File
@@ -30,8 +30,12 @@ server {
access_log /dev/stdout combined; access_log /dev/stdout combined;
error_log /dev/stderr warn; error_log /dev/stderr warn;
# The page is revalidated on every load, so a browser never keeps a
# copy that names assets a newer build no longer has.
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
add_header Cache-Control no-cache;
include /etc/nginx/security-headers.conf;
} }
# Cache static assets aggressively # Cache static assets aggressively
+19
View File
@@ -7,6 +7,7 @@
# headless Chrome at it over CDP, and asserts on computed layout at every # headless Chrome at it over CDP, and asserts on computed layout at every
# viewport width derived from the app's own CSS. See # viewport width derived from the app's own CSS. See
# test/viewport/README.md for what this covers and what it cannot. # test/viewport/README.md for what this covers and what it cannot.
# Before that, it checks the headers the page is served with.
# #
# Deliberately not part of script/check: it needs Docker and takes far # Deliberately not part of script/check: it needs Docker and takes far
# longer than the 20s budget make test has to stay inside. # longer than the 20s budget make test has to stay inside.
@@ -75,6 +76,24 @@ main() {
-v "$ROOT/configs/security-headers.conf:/etc/nginx/security-headers.conf:ro" \ -v "$ROOT/configs/security-headers.conf:/etc/nginx/security-headers.conf:ro" \
"$SERVER_IMAGE" > /dev/null "$SERVER_IMAGE" > /dev/null
# The page, and a path with no file of its own, which falls back to
# it, must go out with Cache-Control: no-cache. That add_header in
# its location drops the server's own unless the location includes
# the security headers again; Content-Security-Policy stands for
# them. curl retries until nginx is listening.
for path in / /no-such-page; do
headers="$(docker exec "$SERVER" curl -sS -o /dev/null -D - \
--retry 10 --retry-connrefused --retry-delay 1 \
"http://127.0.0.1:8080$path" | tr -d '\r')"
if ! printf '%s\n' "$headers" | grep -qix 'Cache-Control: no-cache' ||
! printf '%s\n' "$headers" | grep -qi '^Content-Security-Policy:'; then
echo "frontend-viewport-test: $path needs" \
"Cache-Control: no-cache and the security headers; got:" >&2
printf '%s\n' "$headers" >&2
exit 1
fi
done
# The image's own entrypoint already exposes CDP on 9222 and passes # The image's own entrypoint already exposes CDP on 9222 and passes
# --no-sandbox, so only extra flags belong here; re-specifying the # --no-sandbox, so only extra flags belong here; re-specifying the
# debugging port collides with it and leaves the endpoint bound to # debugging port collides with it and leaves the endpoint bound to