I’ve been working on some web server middleware, and hit a weird issue that I couldn’t find documented anywhere. First, let’s look at an overview of how browser caching works:

If your web server sends an ETag header in a HTTP response, the web browser may choose to cache the response. Next time the same object is requested, the browser may add an If-None-Match header to let the server know that the browser might have the object cached. At this point, the server should respond with the 304 Not Modified code and skip sending the response. This can also happen with the Last Modified and If-Modified-Since headers if ETag is not supported as well.

After implementing this in my middleware, I made a quick test website to try it out. That’s when I ran into a weird behavior: the browser would revalidate the HTML page itself with the If-None-Match header, but when the server responded with 304 it would not attempt to revalidate the linked stylesheets, scripts, and images. The browser would not request them at all and immediately use the cached version. It looks like if the server responds with 304 on the HTML page, the browser assumes that all the linked assets are not modified as well. That means that if the asset does change (you weren’t using something like fingerprinting or versioning on your assets), then the browser will use outdated assets. Oops!

Luckily it looks like there’s an easy solution: add Cache-Control: no-cache header to your responses. no-cache doesn’t actually mean “don’t cache at all”, but rather means that the browser needs to revalidate objects before using the cached version.

Without the Cache-Control header: Browser developer tools window, there is only 1 request for / With the Cache-Control header: Browser developer tools window, there are 5 requests in total, including /, style.css, and 3 images.