SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: Absent and empty are two values: Django's cache merged them and served pages to the wrong user

· by Serguey Asael Shinder / Serguey Shinder

One of the six fixes in Django's security releases of 3 November is a one-line idea with a large consequence. UpdateCacheMiddleware and cache_page() generated the same cache key for a request that omitted a header listed in the response's Vary header and a request that sent that header with an empty value. A response cached for one could be served to the other, which the Django team says "could lead to the exposure of private data". Keys now distinguish the two, "as required by RFC 9111". (https://www.djangoproject.com/weblog/2026/nov/03/security-releases/)

The pattern is not about Python or HTTP. Wherever a value is optional, there are at least three states: not there, there and empty, and there with content. Code that collapses the first two into one, usually by writing a default such as "" or null when something is missing, has thrown away information that someone later needs. In a cache key that someone is the next user.

Serguey Asael Shinder: Absent and empty are two values: Django's cache merged them and served pages to the wrong user
Absent and empty are two values: Django's cache merged them and served pages to the wrong user — Serguey Asael Shinder

Java has the same trap in its own standard library. Map.get returns null both when the key is absent and when it maps to null; only containsKey tells them apart, and getOrDefault returns null for a key explicitly mapped to null rather than the default (a note here on 9 October covered that). A request header read through a helper that returns "" for "not sent" has the same shape. So does a database column where NULL and '' are both used to mean "no value", or a form field where an untouched input and a cleared input reach the server identically.

Three habits keep the states apart. Represent absence with a type that cannot be confused with a value: Optional, a sentinel object, or a separate boolean. Build keys from the raw presence, not from a normalised value, so that "missing" and "empty" serialise differently. And when a value goes into anything that decides who sees what, such as a cache key, a permission check or a deduplication key, write the test with the header missing, the header empty and the header set, because the first two look the same in every happy-path test.

The Django fix also comes with an honest side effect: responses cached under the old keys will no longer match and will be cached again. A correct key costs one round of cache misses. A merged key costs a privacy incident.