Wait, a 304 reply can change headers?

This evening I was optimizing some caching things for a web app I’ve been working on when I discovered something obvious when you think about it. Now, this is specific to Chrome-ish browsers and Firefox; I have no idea what will happen in Safari.

Imagine the following PHP code:


$etag = md5('hello world');
$egg  = random_int(1, 100);

// despite saying "no cache" we actually do cache
// and we validate it on every request:
// https://csswizardry.com/2019/03/cache-control-for-civilians/#no-cache
header('Cache-Control: no-cache,private');
header('Etag: ' . $etag);
header('Easter-Egg: ' . $egg);
setcookie('Easter-Egg', $egg, 0, '/', '', true, true);
header('Vary: Accept-Encoding');

if (str_replace('"', '', $_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === $etag) {

echo $egg;

This code sets a random integer and outputs it in a cookie, header, and on the page. After first loading the page, the screen’s number won’t change. However, the changing headers are accepted. You’ll see the number change in dev-tools (but if you look at the response in dev-tools, you’ll see it unchanging) and the cookie updating as well.

Now, this can go horribly wrong if a public cache is doing the caching (i.e., a CDN or proxy) because I’m 99% sure (without testing it) that those things will probably cache the headers. So, if you don’t set your cache to private, it is quite likely you want to ensure your headers never, ever change.

But this means some really amazing things for web apps and APIs because this means you can “smuggle” things through to the browser out-of-band (like nonces), even if the requested data hasn’t changed. Why would you do this? For performance reasons on shitty networks, that’s why. 😉

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.