Files
werkator/docs/prs/2026-09-09-PR#45-token-cookie-and-caching.md
3578aeb0fb fix: never store an answer that depends on the token cookie (#45)
Since PR#43 a page and every JSON answer differ by the control-token cookie,
and both were sent without a single caching header. So a browser could
redisplay the copy it already had after a token was pasted, which is what made
a newly entered token look ineffective — and a shared cache in front of
Werkator was free to hand one visitor's answer to another, in the worst case
the operator's page with every repository named.

The pages and the JSON APIs are no-store now and say they vary by cookie.
Artifact files keep the caching they have, because a report is large and never
changes under its key; they only state the dependency, which is enough for a
correct shared cache.

The key in the header asks before forgetting the token, and after taking one it
fetches the address afresh instead of reloading.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #45
2026-09-09 20:05:11 +02:00

3.8 KiB

WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.

  • PR#43 — made the answers depend on the control-token cookie, which is what these two corrections follow from.

The Problem

A newly entered control token did not take effect. Since PR#43 a page and every JSON answer depend on the control-token cookie: a private repository is redacted for a visitor and whole for the operator, and the actions are disabled or not. Those answers were sent without a single caching header — no Cache-Control, no Vary, not even a validator — so after pasting the token the browser could redisplay the copy it already had, and the token looked as if it had not worked.

The same omission is a privacy problem in its own right, and a worse one: a shared cache in front of Werkator — the reverse proxy every deployment runs behind — was free to store a response and hand it to somebody else. That could show a visitor's redacted page to the operator, or the operator's page, with every repository named, to a visitor.

The key threw the token away on a single click. It sits next to the reload button in the header. A misclick cost the token, which then has to be fetched from the host again.

Non-Goals

  • No change to what is shown to whom. This PR is about the answers arriving fresh, not about their content.
  • No caching strategy for artifact files beyond what they already have; they only gain the Vary that states the truth.

The Scenarios

Feature: answers that depend on the token

So that a token works the moment it is entered, and no cache can mix two visitors up.

  • Given any page or JSON answer of the UI, the builds API, the repositories API or the system API
  • When it is requested
  • Then it carries Cache-Control: no-store
    • and it carries Vary: Cookie
  • Given an artifact file, which depends on the cookie only in whether it exists at all
  • Then it keeps the caching it has and carries Vary: Cookie
    • because a report is large and never changes under its key, and stating the dependency is enough for a correct shared cache
  • And when a control token is entered and the address fetched again
  • Then the answer is the new one, not the one the browser had
Verified by

Feature: the key in the header

Scenario#45.02: Forgetting the token is asked for, entering it is not

  • Given a browser holding the token
  • When the key is clicked
  • Then it asks before forgetting it, naming what will be hidden again
  • Given a browser without one
  • When the key is clicked and a token entered
  • Then it is stored and the address fetched afresh, so the page comes back in its operator form

The Solution

CookieDependentResponses is a one-interceptor configuration naming the controllers whose answer depends on who is asking, and setting no-store and Vary: Cookie on them. no-store rather than a validator: these answers are small, they are polled anyway, and they are meaningless a moment later.

The key button asks before forgetting, and after taking a token it uses location.replace(location.href) rather than reload() — a reload may repeat the request that produced the page, while replacing always fetches the address afresh, now with the cookie that exists since a moment ago.

Additional Changes

  • Version 1.5.2 and its release-notes entry, since this is what gets deployed.