It seems caching of resources in browsers is broken when HTTP/2 and compression is enabled. I am not sure if this is a Spring Boot or Tomcat bug. I suspect it is being caused by the wrong Content-Length header being sent.
It seems the Content-Length header contains the length of the content before compression. IIRC, it should contain the length of the content after compression. I think this causes browsers not to cache the resources.
I have a sample project here: https://github.com/MikeN123/spring-boot-cache-sample Problem can always be reproduced in Firefox, and is also reproducible in Chrome if you are not in an incognito session and close the window between requests.
Instructions:
- Clone project
- Assign a valid SSL cert in
application.properties
, using a tool like mkcert. Note this is required, with an invalid cert, browsers will not cache anyway. - Start project
- Navigate to https://localhost:8443/index.html in an incognito window in Firefox, with network inspector open
- Click on the link to navigate to
index.html
again. Notice in network inspector thatjquery.js
is being fetched again. It should not have been fetched again, asmax-age
is set to 1 year. Also note that the content-length of thejquery.js
request shows the original content-length, before compression. - Retry these steps, but set
server.http2.enabled=false
orserver.compression.enabled=false
. Notice thejquery.js
resource does get cached properly now.
Screenshot of the network inspector showing jquery.js
being reloaded:
Comment From: wilkinsona
Thanks for the sample, @MikeN123. Thanks, too, for the pointer to mkcert as I wasn't aware of it. I've reproduced the problem using Tomcat. Interestingly, I've also reproduced it using Undertow and it does not send a content-length
header in the response:
HTTP/2 200 OK
content-encoding: gzip
vary: Origin
vary: Access-Control-Request-Method
vary: Access-Control-Request-Headers
last-modified: Fri, 01 May 2020 14:21:30 GMT
cache-control: max-age=31536000
content-type: application/javascript
accept-ranges: bytes
date: Fri, 01 May 2020 14:29:22 GMT
X-Firefox-Spdy: h2
Comment From: MikeN123
That's odd. Did you open a new incognito window? From my experience, once a certain URL starts to give varying responses, browser refrain from caching it (e.g., if I disable compression but not re-open my browser window, it still does not start caching the resource).
I have the an application that is exhibiting this problem also running behind an nginx proxy w/ http/2 and compression, and there the browsers do seem to cache the resources. So it does have something to do with the web server, but I still can't quite put my finger on the exact cause of the issue.
Comment From: wilkinsona
That's odd. Did you open a new incognito window?
No. Firefox fails with SSL_ERROR_NO_CYPHER_OVERLAP
in a private browsing window while being able to complete the SSL handshake in a normal window. Googling suggests that's a Firefox bug.
I've switched to Chrome now where an incognito window works fine. Going back to Tomcat with HTTP/2 and compression enabled, Chrome caches jquery.js as expected. I've just tried Safari as well in a private window and it too caches jquery.js as expected. As far as I can tell, the problem appears to be specific to Firefox. Best of luck with tracking down the problem, but I don't think there's anything more we can do to help you here.
Comment From: MikeN123
I disagree, Chrome does never save the jquery.js in the disk cache (only memory cache) and closing and reopening the window causes jquery.js to be retrieved again.
Also, I am still quite sure the content-length
header is invalid and that this is triggering the issue.
With Undertow and Firefox it works fine for me, further strenghtening my believe this is a Tomcat issue. Maybe @markt-asf knows a bit more about this?
Comment From: wilkinsona
I disagree, Chrome does never save the jquery.js in the disk cache (only memory cache) and closing and reopening the window causes jquery.js to be retrieved again.
That's really a detail of how Chrome decides to cache, not whether or not something can be cached. It's also not what I see outside of an incognito window. I performed the following steps to see how Chrome behaves:
- Clear Chrome's cache
- Open https://localhost:8443/index.html
- Click "Navigate to this page again."
- Quit Chrome
- Launch Chrome and open https://localhost:8443/index.html
At step 2, jquery.js is requested from the server. At step 3 it comes from the in-memory cache. At step 5 it comes from the disk cache.
Comment From: MikeN123
I still don't see Chrome caching it. I'm quite sure this is a bug in Tomcat, I will file a bug with them, and it is caused by the Content-Length header that should not be send.
The issue is that the prepareResponse
for Http11 first checks compression (which unsets content-length) and then sets a content-length header if necessary. Http2 inverses that order, which causes it to set a content-length header it should not set.
[edit] For reference: https://bz.apache.org/bugzilla/show_bug.cgi?id=64403
Comment From: MikeN123
In case anyone else is seeing this issue: Tomcat 9.0.35 is now available and fixes this issue. Will probably be available in Spring Boot 2.2.8.