Skip to content

Serve HTTP validators on the remaining server-rendered pages - #760

Draft
Wavesonics wants to merge 1 commit into
developfrom
feature/page-etag-validators
Draft

Serve HTTP validators on the remaining server-rendered pages#760
Wavesonics wants to merge 1 commit into
developfrom
feature/page-etag-validators

Conversation

@Wavesonics

Copy link
Copy Markdown
Collaborator

Follow-on to #758, which added pageETag / matchesETag / applyRevalidationHeaders and used them only on the public story reader.

What changed

Home, author, about, privacy and terms now answer 304 to a reader who already holds the page. They go through one new helper, ApplicationCall.respondPage(template, model, vararg extra), which hashes the completed Mustache model just before responding.

Hashing at respond time rather than wiring matchesETag + applyRevalidationHeaders into each page means every value that shapes the page is in the validator by construction — a field added to a page joins its validator instead of quietly serving a stale copy. Anything that shapes the response without living in the model must be passed as extra; on the author page that is community_member, which gates the X-Robots-Tag header.

The public story reader keeps its own early-hash path and is deliberately not converted. It has a cheap fingerprint available before the expensive render, so it can skip the render entirely — something respond-time hashing cannot do. The two patterns are not interchangeable, and the other pages have no such cheap probe.

Staleness bug this exposed

pageETag skips the bulky msg bundle, on the grounds that it is fully determined by the hashed locale and version entries. The msg(model, …) overload breaks that invariant: it writes messages formatted at request time into that same map.

On the home page this hid the admin contact email from the validator — it reaches the page only through home_servermessage_whitelist and appears nowhere else in the model. An admin changing the address would produce an identical ETag, and every browser holding the old one would be served a 304 with the stale address indefinitely.

Fixed at the source rather than by hand-wiring contactEmail into one page's pageETag call: formatted messages are mirrored into a hashed model key, so any future use of that overload is covered automatically.

Written test-first per the repo convention. With the mirror removed, the new test fails with exactly the predicted symptom (a changed contact address must not answer 304).

Is it worth it? Measured, not assumed

Through the EndToEndTest harness:

request per-request
GET / (full render, 16.7 KB body) ~320 ms
GET / with If-None-Match → 304 ~320 ms
GET /about (one-line body) ~157 ms
GET /robots.txt (floor) ~1.2 ms

The 304 path is not measurably faster than a full render. Essentially none of a page's cost is the Mustache render or the body — it is all model construction, before the template is touched. Roughly 157 ms is a baseline every page pays in the withDefaults / withMessages shell; home adds ~160 ms of its own.

Absolute numbers are harness-inflated (SQLite over a FakeFileSystem, debug logging), so treat them as a ratio rather than a production figure. The ratio is the point, and it says the remaining benefit here is bandwidth only — ~16 KB per revalidation, which is real for mobile readers but is not a server-load argument. That baseline page latency looks like the more valuable target and is worth its own investigation; it is out of scope here.

Testing

  • 9 new tests in PageValidatorTest.kt, driving the real routes through the EndToEndTest harness: validator present and weak, Cache-Control / Vary, the 304, and invalidation after a server-message, contact-email, about-text, bio, or community-membership change.
  • Full server suite green: 965 tests, 0 failures.

Home, author, about, privacy and terms now answer 304 to a reader who
already holds the page, through a respondPage helper that hashes the
completed Mustache model just before responding. Hashing there rather
than per-page means every value shaping the page is in the validator by
construction; anything that shapes the response without living in the
model, such as the X-Robots-Tag that community_member gates on the author
page, is passed explicitly.

The public story reader keeps its own early-hash path: it has a cheap
fingerprint available before the render, so it can skip the render
entirely, which respond-time hashing cannot do.

Fixes a staleness hole this exposed. pageETag skips the bulk msg bundle
on the grounds that it is determined by locale and version, but the
msg(model, ...) overload writes messages formatted at request time into
that same map. On the home page that hid the admin contact email from
the validator, so changing the address would leave every browser holding
the old one on a 304 forever. Formatted messages are now mirrored into a
hashed key, covering future uses without per-page wiring.

Measured through the e2e harness, the 304 path is not faster than a full
render: the cost of these pages is model construction, not the template
or the body. The win is bandwidth, roughly 16 KB per revalidation.
@codacy-production

Copy link
Copy Markdown
Contributor

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · 0 duplication

Metric Results
Complexity 0
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@Wavesonics
Wavesonics marked this pull request as draft July 21, 2026 22:35
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.

1 participant