Server sometimes responds with HTTP 200 even when If-None-Match matches ETag

Hi,

We’re experiencing intermittent slow page loads on our Netlify website. Some debugging points to what seems to be an intermittent bug in Netlify web servers related to caching and compression. I’ve searched the forums but couldn’t find a post on this issue.

Specifically, we have a large JavaScript resource at the following URL:
https://cantamus.app/packages/verovio-3.3.0/verovio.js

Usually the browser correctly uses the cached version, since the server returns HTTP 304. However in some cases, the browser re-downloads the resource since the server returns HTTP 200, even though the browser sent a correct If-None-Match header. It seems that in those ‘cache miss’ requests, the server returns a wrong ETag header which does not include the -df suffix (even though the request was for the compressed resource).

I’ve verified this in two ways:

  1. Accessing the above URL in my browser (Chrome) with the DevTools open, and seeing that sometimes HTTP 200 is sent instead of HTTP 304, even though the browser sent If-None-Match correctly.

  2. Using curl with the following command:
    curl --compressed "https://cantamus.app/packages/verovio-3.3.0/verovio.js" -H "If-None-Match: f1ee2cc0ca9c51f2d05575f8c1436407-ssl-df" -I
    And observing that this request intermittently gives 200 and 304 responses. See screenshot below of two such requests (but I’ve reproduced this dozens of times).

Needless to say the resource itself is not being updated.

Further debugging:

I fixed the curl command to include double quotes in If-None-Match, but the problem can still be reproduced:

When requesting the non-compressed version (with the corresponding ETag - omitting -df), I have till now NOT managed to reproduce the problem:

When requesting the non-compressed version without quotes in If-None-Match, I seem to always get HTTP 200 (never 304):

So in summary, the problem only seems to be for compressed resource requests.

Further debugging: It seems that curl’s --compressed flag passes an Accept-Encoding header slightly different from Chrome’s.

curl: Accept-Encoding: deflate, gzip
Chrome: accept-encoding: gzip, deflate, br

If I use the header from Chrome in curl, I can’t right now reproduce the issue. Although it definitely can be reproduced this way, because I first noticed the problem in Chrome in the first place.

Also, the server sometimes returns a different ETag for the same request…
At this point I’m giving up and waiting for the support team’s reponse :slight_smile:

Just caught a slow page load with Chrome DevTools open, where HTTP 200 was returned for the script even though the resource ETag matched the browser’s If-None-Match. Also, for some reason the download was very slow (normally it would take a few hundred milliseconds but here it took 12.44 seconds for the content download).

hi there @matan, thanks for sharing - i am still hoping to get some :eyes: on this soon.

Hey there, thanks for your patience!

So, I delved in to this and I could see that the d42d7fe7-a8e6-411d-bb9c-51fac47f983f request was uncached. This can happen for several reasons:

  • Our cache opted to evict this from the store
    • The asset may not have been requested for some time, or
    • The asset is of considerable size
  • The fingerprinted asset changed (a new build, file change)
    • You seem to have this accounted for

Just a head’s up RE: fingerprinted assets, it’s not advisable when using our CDN. By fingerprinting your assets, it’s likely that file names for all of your assets change after every build, even if the file does not change. This means that the asset becomes uncached after each build. You can read more about how we identify file changes without needing file name hashes here.

But, in your case, the eviction is likely due to the asset’s size (~1.5MB). If this particular JS asset is blocking your initial render, you should definitely try to break it up and load some of it asynchronously/lazily – the smaller, the better, with sub-500kb being the sweet spot and sub-1MB being a good benchmark.

Larger files are more regularly contested and subsequently evicted from our cache.

To add to this, and in general, you may experience slow loads on our CDN, if a CDN node hasn’t cached the file before. In each region where we have a CDN presence, we have multiple nodes and they each have their own cache. If the node which responds to your request hasn’t cached the asset, the node must request the asset (uncompressed) from our west-coast US origin server. The asset is then sent uncompressed to the CDN node, compressed, then sent to the browser. This is a one-off longer load. Each node will preserve the asset until your site is re-deployed and the asset changes.

I hope that this helps!

Hi, thanks for your response! This was really helpful.

I guess one simple workaround/solution for us could be to override the caching headers to remove max-age=0, must-revalidate. Then we would be relying much less on the CDN cache. This is an external library and basically a static resource (unless we upgrade to a newer version of the library and then the name changes).

The loading of this library is not blocking initial render, but we do have to rely on it for displaying the main page content (the library is a WASM-compiled musical score renderer, and the main page content is the musical score). We will try to reduce the library size though.

P.S. Maybe your CDN should compress assets when sending them between nodes that are far away :slight_smile:

Btw also I’m not sure it’s the best strategy to evict large files from the cache, for them the cache is most important!

Depends how often they’re called! Low traffic sites might see their content booted more often. Not to mention the warm-up time if a new node enters rotation.

Content is compressed, btw, during transit from CDN to requester (just clarifying for anyone who ignores the wall of text in future!)