1 Commits
Author SHA1 Message Date
sneak 33859b1dc1 Page is sent with Cache-Control: no-cache (closes #141)
check / check (push) Canceled after 0s
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
2026-10-18 23:50:38 +00:00
4 changed files with 21 additions and 28 deletions
+6 -8
View File
@@ -86,10 +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)). Before the browser [test/viewport/README.md](test/viewport/README.md)). It first checks that the
starts, it checks that the page, and a path that falls back to it, are served page is served with `Cache-Control: no-cache` and the security headers. Not
with `Cache-Control: no-cache` and `Content-Security-Policy`. Not part of part of `script/check`: it takes minutes.
`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`,
@@ -437,10 +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
- Sends the page with `Cache-Control: no-cache`, so a browser that keeps a copy - Sends the page with `Cache-Control: no-cache`, so the browser revalidates it
checks with the server that it is current before using it again; only the on every load; only the files under `/assets/`, whose names change with every
files under `/assets/`, whose names change whenever their contents do, are build, are cached as immutable
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`
+9 -11
View File
@@ -22,17 +22,15 @@ 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` - 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 a browser that ([#141](https://git.eeqj.de/sneak/netwatch/issues/141)), so the browser
keeps a copy checks with the server that it is current before using it again. revalidates it on every load. It went out with no `Cache-Control`, so a
The page went out with no `Cache-Control`, so a browser could keep it after a browser could keep it after a deploy and ask for files under `/assets/` that
deploy and ask for files under `/assets/` that the new build no longer has, the new build no longer has, and show an empty page. A path with no file of
and show an empty page. A path with no file of its own falls back to the page its own falls back to the page and gets the same header. That location now
and gets the same header. `location /` now includes the security headers includes the security headers again, as `/assets/` does, since its own
again, as `/assets/` does, since its own `add_header` drops the server's. `add_header` drops the server's. `/assets/` keeps its immutable caching.
`/assets/` keeps its immutable caching. `make frontend-viewport-test` fails `make frontend-viewport-test` fails unless `/` and a path that falls back to
unless `/` and a path that falls back to the page carry the page carry `Cache-Control: no-cache` and the security headers
`Cache-Control: no-cache` and `Content-Security-Policy`, which comes from the
same file as the other 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
+2 -3
View File
@@ -30,9 +30,8 @@ server {
access_log /dev/stdout combined; access_log /dev/stdout combined;
error_log /dev/stderr warn; error_log /dev/stderr warn;
# With no-cache, a browser never uses a copy of the page that names # The page is revalidated on every load, so a browser never keeps a
# assets a newer build no longer has without first checking with the # copy that names assets a newer build no longer has.
# server that it is current.
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
add_header Cache-Control no-cache; add_header Cache-Control no-cache;
+4 -6
View File
@@ -7,8 +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 the browser starts, it checks that the page is served with # Before that, it checks the headers the page is served with.
# Cache-Control: no-cache and Content-Security-Policy.
# #
# 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.
@@ -80,9 +79,8 @@ main() {
# The page, and a path with no file of its own, which falls back to # 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 # it, must go out with Cache-Control: no-cache. That add_header in
# its location drops the server's own unless the location includes # its location drops the server's own unless the location includes
# the security headers again; only Content-Security-Policy is # the security headers again; Content-Security-Policy stands for
# checked, as it comes from the same file as the rest. curl tries up # them. curl retries until nginx is listening.
# to ten more times, a second apart, while nginx starts.
for path in / /no-such-page; do for path in / /no-such-page; do
headers="$(docker exec "$SERVER" curl -sS -o /dev/null -D - \ headers="$(docker exec "$SERVER" curl -sS -o /dev/null -D - \
--retry 10 --retry-connrefused --retry-delay 1 \ --retry 10 --retry-connrefused --retry-delay 1 \
@@ -90,7 +88,7 @@ main() {
if ! printf '%s\n' "$headers" | grep -qix 'Cache-Control: no-cache' || if ! printf '%s\n' "$headers" | grep -qix 'Cache-Control: no-cache' ||
! printf '%s\n' "$headers" | grep -qi '^Content-Security-Policy:'; then ! printf '%s\n' "$headers" | grep -qi '^Content-Security-Policy:'; then
echo "frontend-viewport-test: $path needs" \ echo "frontend-viewport-test: $path needs" \
"Cache-Control: no-cache and Content-Security-Policy; got:" >&2 "Cache-Control: no-cache and the security headers; got:" >&2
printf '%s\n' "$headers" >&2 printf '%s\n' "$headers" >&2
exit 1 exit 1
fi fi