3

I have some image resources that are immutable and can be cached forever. Chrome seems to respect my response headers, and does not re-validate the resources:

Chrome requests

Here's an example of one of these resources in Chrome. As you can see, I include cache-control: public, max-age, expires, etag and last-modified and the resource is served from "memory cache":

Chrome request


Firefox, however, does not respect these headers and re-validates the resources on every load! My server is hit with a request for each profile pic every time the page loads, and returns a 304:

Firefox requests

Here is an example of on such request that results in a 304:

Firefox request

I can't figure why Firefox ignores the cache headers and keeps going to the server for the 304. I've experimented with various cache-related headers and read the standard on what's "cacheable". I've ensured that caching is enabled in devtools. I've tried with devtools closed too, and I keep seeing the 304s in my server logs.

I've discovered that this only happens on a page refresh. A plain refresh, though, not shift- or shift-command-, but just a plain refresh. That's not the behavior I was expecting.

Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102
Dmitry Minkovsky
  • 36,185
  • 26
  • 116
  • 160
  • @KevinChristopherHenry thanks you for asking. I meant to mention that in my question: I did check that several times, and I **do not** have that enabled. I kept hoping that was the problem... – Dmitry Minkovsky Mar 28 '20 at 16:30
  • I tried with devtools closed too, and I did keep seeing the 304s in my server logs. Yes, the `max-age=0` I don't understand. I've been accessing the page via refresh (command/control+r), but I'll try it out with page navigation. That's interesting. > "There are a number of answers here about that." I'll search, thank you. – Dmitry Minkovsky Mar 28 '20 at 17:29
  • Hey, I can confirm that mode of navigation does affect the behavior. Using browser back/forward or app-based routing, Firefox does not issue requests! It's the refresh that causes this behavior. A plain refresh, though, not shift- or shift-command-, but just a plain refresh. That's not the behavior I was expecting. – Dmitry Minkovsky Mar 28 '20 at 17:59
  • @KevinChristopherHenry if you want to post the answer I'll vote up/accept. Thanks for your help. – Dmitry Minkovsky Mar 28 '20 at 21:36

2 Answers2

5

Put simply: Firefox revalidates cached content on browser refresh.

That used to be what all browsers did. It was probably reasonable at one time to assume that if a user was actively refreshing their page it was because something went wrong and they needed to start from scratch. Now, with the advent of sites showing real-time content, the use of "refresh" may be quite different.

Chrome and Firefox seem to be taking different paths to deal with this. Chrome's approach is to make its refresh behavior smarter and more sophisticated, while Firefox has chosen to rely on the developer explicitly indicating that a non-stale resource never needs revalidation by using the Cache-Control: immutable response header. (See this answer for more on this distinction).

If this refresh behavior is an important use case for your application (as opposed to just something you're using for debugging purposes) adding Cache-Control: immutable should solve this problem in Firefox. (Note that, according to MDN, Firefox only respects immutable on https content, so this still wouldn't work on your http test page above.)

Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102
  • Thanks for the historical context. N.B. I tried `Cache-Control: immutable` to no effect on refresh in Firefox. That is, I tried adding it as a directive alongside `max-age` and `public`. I didn't try it as a standalone directive, because Chrome is not listed as supporting it, and I didn't feel like experimenting with multiple `Cache-Control` headers. I'm fine with it just re-validating on refresh as long as that's the expected behavior for Firefox. – Dmitry Minkovsky Mar 29 '20 at 19:16
  • Haven't tried it yet, but I think `stale-while-revalidate` might be useful here too: https://web.dev/stale-while-revalidate/. Given my cursory skim of that page, at least the "stale" image will be shown while the 304 is retrieved. – Dmitry Minkovsky Apr 05 '20 at 18:41
  • @DmitryMinkovsky: That might be a good solution if your concern is with the responsiveness of the page (as opposed to the load on the server, since the number of requests is the same). Even if the browser supports it in general, though, it still may not use it on refresh. `immutable` seems the better solution, though I'm not sure why it isn't working in your case. – Kevin Christopher Henry Apr 06 '20 at 05:38
  • 2
    No idea why `immutable` isn't working either. I just tried again adding it to my list of cache-control directives. And no luck. No matter what, on refresh, FF revalidates. `stale-while-revalidate` doesn't seem to speed things up either, plus it revalidates. I guess that's how just FF works. – Dmitry Minkovsky Apr 07 '20 at 01:04
  • 1
    Looks like MDN now explains: "In Firefox, immutable is only honored on https:// transactions. For more information, see also this blog post." Pretty sure I was running locally and over http:// when I posted this question: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control – Dmitry Minkovsky Jun 29 '21 at 03:14
  • @DmitryMinkovsky: Good find, thanks for the update. Yes, your screenshots show that you were using `http`. I've edited the answer. – Kevin Christopher Henry Jun 29 '21 at 13:56
1

Just to be more verbose let me elaborate on ETags. Technically, Firefox is handling it correctly. When an Etag is present, the client or intermediary caches need to perform a GET or HEAD ensuring the content being served hasn't changed.

The If-None-Match header tells the server what version or versions it has. If it has not changed, the server/proxy returns a 304.

You can modify the revalidation behavior by making it a weak ETag. That is done by including W/ in front of the ETags value, i.e. ETag: /W #########. This can be handled on your server or if you can't control it by rewrite rules on your CDN.

The possible workaround for Firefox is adding the Cache-Control Immutable options. Cache-Control: Immutable. Without looking at the code in Firefox caching heuristics I can't say for sure but it should be easy to test.

Other servers will ignore the header if they don't support it.

Etags also don't work with byte-range requests. So if you don't have a specific reason for using them consider turning them off.

  • Hey I'll try that, thanks. `If-None-Match` is beyond my control... it's included because I include an `Etag`. I can't remember if I tried not including an `Etag`. I will try that again. I was looking at `immutable` but I wasn't sure if I could specify it alongside `public, max-age` and how it would interact with other browsers that don't support it. I just left this comment, too: https://stackoverflow.com/questions/60902008/why-does-firefox-ignore-cache-headers-and-re-validates-responses-that-should-be?noredirect=1#comment107753570_60902008 – Dmitry Minkovsky Mar 28 '20 at 18:00
  • Do you need the Etag? If so, you can also try converting it to a weak Etag that allows cached copies to be used. Etag: W/.............. Here is the wiki post on them, https://en.wikipedia.org/wiki/HTTP_ETag#Strong_and_weak_validation. – John Scharber Mar 28 '20 at 18:20
  • No, the presence of an `ETag` does not prevent the browser from caching. It just provides a mechanism for revalidating the resource when it does expire. The behavior the OP is seeing is because they're refreshing the page, and a refresh in Firefox triggers revalidation. If the revalidation headers weren't there the refresh would result in an even more costly regular request. – Kevin Christopher Henry Mar 28 '20 at 20:19
  • He still has expires and last-modified so the result would still be an IMS to the server, just like he is getting. Most of my experience comes from building three CDNs, I can't really say what Firefox does, but I know how we would have treated it. – John Scharber Mar 28 '20 at 20:59
  • Yeah it looks like this is discussed over here: https://stackoverflow.com/a/30181543/741970. The `max-age=0` and revalidation is because I was refreshing. Back/forward and history API navigation doesn't cause revalidation. Interesting how Chrome doesn't do this. I did test adding `immutable` and removing the `etag`, but that didn't change the behavior. @KevinChristopherHenry – Dmitry Minkovsky Mar 28 '20 at 21:16