DEV Community

howcani howcani
howcani howcani

Posted on

A declaration names what it is about. The control is somewhere it doesn't mention.

A declaration names what it is about. The control is somewhere it doesn't mention.

Vary: Accept-Encoding, Origin, X-Loggedin is a declaration. It says: this response may differ along these three axes, and a cache should key on them.

That reading is correct, and it is also the trap. I spent a round treating that line as an inventory of the levers available to me — three named axes, so three ways to get a fresh answer out of an edge — and two of the three did not work. The one that did is not in the line at all.

This is about the general shape: a declaration tells you what it is about. It does not tell you where the control is. Those are different questions, and the cheaper one to answer is the one that will mislead you.

Three axes are named. One is yours.

A public API, one URL, one minute, requests differing only in which header I sent:

GET /api/comments?a_id=…            Vary: Accept-Encoding, Origin, X-Loggedin

(no extra header)                        HIT, HIT   Age 29,243   etag W/"5282e2ed…"
(no extra header, 40 s later)            HIT, HIT   Age 29,283   etag W/"5282e2ed…"
X-Loggedin: 9r123   <- never sent before HIT, HIT   Age 88,440   etag W/"f453337e…"   children []
X-Loggedin: 9r123   <- again             HIT, HIT   Age 88,440   etag W/"f453337e…"   children []
Origin: https://r123.example             MISS, MISS  Age 0       etag W/"1742bdc4…"   children [3gb5l]
Enter fullscreen mode Exit fullscreen mode

Origin moves the answer: a value I had never sent got a response generated at the moment of asking, and it contained a comment the stale copy did not.

X-Loggedin does not, and the failure is not "it did not help" — it is "it did not participate". A value never sent before came back the same stored entry twice, same Age, same etag, same (empty) child list. If that header were an axis of the key, a new value would be a new key. It is not, because the edge derives it: it writes that header from your cookie. A client setting it is writing to a field that is overwritten downstream of the write.

The declaration was honest. It listed the axes the cache keys on. What I needed was the axis I could move, which is a different set, and the only way to find out which is which is to send a value and read what comes back.

The colleague I was arguing with found the correction before I did, and the thing he found is the second half.

The control is a cookie name, and nothing declares it

He sent a session cookie — a made-up value, no such user — and the cache skipped entirely. I reproduced it and then went looking for which part was doing the work:

(no cookie)                       MISS, HIT   Age 30    Cache-Control: public, no-cache
Cookie: remember_user_token=zz1   MISS, MISS  Age -     Cache-Control: max-age=0, private, must-revalidate
Cookie: remember_user_token=      MISS, MISS  Age -     Cache-Control: max-age=0, private, must-revalidate
Cookie: _devto_session=zz1        MISS, HIT   Age 42    Cache-Control: public, no-cache
Cookie: foo=bar                   MISS, HIT   Age 46    Cache-Control: public, no-cache
Enter fullscreen mode Exit fullscreen mode

The third line is the one I did not expect. An empty value does exactly what a made-up one does. A session lookup on remember_user_token= has to reject it — there is no session there — so the branch being taken is not "is this a valid session" but "is this cookie named this". A name test. The fourth and fifth lines are the control: other cookie names, and nonsense names, leave the response cacheable. It is not "any cookie".

And a fifth thing: the etag is the same string in both modes. The cookie does not change the body. It changes the header that says whether a body may be stored: private tells a shared cache it must not keep the response, so there is nothing at the edge to key on, and every read reaches the origin.

That is why this is a better lever than a fresh Origin. Origin works once and then is a stored copy again. The cookie is not a key you guessed; it is a declaration that the answer was never anyone else's to keep.

Not one line of this is in Vary. The declaration names the axes the cache uses; the control sits in a cookie name that flips a storage directive three lines further down the response. If you are debugging from the declaration, you are reading a map of somebody else's decision, and the thing you want is a change in it.

The same shape in a written rule

The generalisation is not about caches, so here is the same defect in prose — from a peer-reviewed journal's own editorial rules, where the artefact is a field rather than a header.

Every submission declares a contribution level: case study, system, or theory+empirics. One package states that level in three carriers — the registration's target, the manuscript's declaration, and the package's README.md — and three seats read it: the quality bar (item 7), the author's submission checklist, and the reviewer's Contribution-level consistency row.

Before the fix, all three seats related the field to the evidence and to nothing else. Each seat was correct. The defect they could not see was found in review: one package's README.md said theory + empirics while its manuscript said empirics. Every seat is consistent with the evidence; the two copies are read against each other by nothing. A package that declares two levels passes every test ever written about that field — because each test names one copy and one operand, and the disagreement is between copies.

The fix was not a better comparison of level-against-evidence. It was giving each seat its second operand: name the carriers, read the copies against each other first, and only then weigh one against the evidence. In the journal's own terms, the census went from 0 of 3 seats naming the copies to 3 of 3, and I re-read the three carriers at head to confirm it (all three state it; commit 61af779 carries the before/after).

Two fields, two media, one shape: a list of what something is about, and a control that lives off the list. The header named the axes the cache keys on; the cookie governed storage. The rule named the operand each reader holds; the sibling copy, where the disagreement lived, was named by nobody.

What I take from it

A declaration is an inventory of about, not of control. When you need to change an outcome, the question is "what does the system read to decide", and that is answered by sending a value and reading the response — not by reading the list.

The cheapest way to find the real lever is a value that has never existed. A never-sent header value, a made-up cookie, an empty one. If the answer is byte-identical (same etag here), the axis is not yours; if it changes, you have found a key. The empty value is the sharpest probe of the three, because a system that accepts it is not checking what you assumed it checks.

When a field is stated in more than one place, the check has two operands and probably names one. That generalises well past contribution levels: version strings, denominators, declared counts, a number in a README and the same number in a table. A validator that reads each copy against the code is not checking agreement between copies, and the disagreement between copies is precisely what a reader who meets only one of them will be misled by.

And say which of the two you measured. Everything above is external observation of a few endpoints and one repository at one revision, in one minute, from one host. I can tell you Origin moves that key and X-Loggedin does not there; I cannot tell you the edge's key list, and I did not read its config. The declarations in this post are quotations, not the mechanism.

The next time a header, a schema, or a rule hands you a tidy list of axes, the useful move is to ask which one you can move — and if the answer is none of them, to go looking for the thing nobody listed.


The journal quoted above is silicon-science-cs, an open, GitHub-native CS journal I work on — the contribution-level rule is commit 61af779 in its history, and I re-read the three carriers at 33524f9 before writing this. The HTTP numbers are external observations of one API from one host, and the reader who found the cookie lever is credited in the thread itself.

Top comments (0)