Cloudflare CDN serves stale response when Origin header is set

I’m getting some peculiar behaviour from Cloudflare CDN cache, let me explain the background first:

Background

I use a pattern where my origin server sets Cache-Control: public, max-age=0, s-maxage=86400 on certain responses and I use the “Cache Everything” page rule so that the CDN will cache the responses for a good while, and browsers will always revalidate with the CDN. Then I use the purge API to purge the CDN when the response is invalidated.

This pattern has worked well on other CDNs like Fastly.

The issue with Cloudflare

Now, with Cloudflare CDN, when I send a purge request .../zones/.../purge_cache like {"files":["https://api.my.site/path/to/api"]}, the cache never get’s purged. But it gets weirder:

  • The browser fetches https://api.my.site/path/to/api (example) using the browser Fetch API
  • When I purge the specific URL, Cloudlfare does not purge the response, it continues to serve the cached response with Cf-Cache-Status: HIT, and the Age header keeps incrementing. The devtools request timing shows that the browser is indeed downloading a response from the Cloudflare server, but Cloudflare is serving the cached response.
  • When I do a “Purge everything”, finally Cloudflare serves a fresh response with Cf-Cache-Status: EXPIRED
  • If I navigate the browser itself to https://api.my.site/path/to/api it will immediately get served a fresh response when I purge the specific path, as I expect from the Fetch API, and then cached response until the next purge. Same if I use Postman, Cloudflare serves a fresh response after purge.

So, Cloudflare continues to serve stale responses to Fetch API after a selective purge while it serves a fresh response to other user agents like browser navigation, Postman etc. And then it only serves a fresh response to the Fetch API after a “Purge everything”.

What am I missing?

1 Like

I think I have the same issue.Purging-all-ULR works well but Purging-single-URL dosent work sometimes.

Ok I’ve narrowed the behaviour down, to the Origin header. When Origin is set (i.e. from a Fetch API request) then Cloudflare serves an old stale response, at least until Purge Everthing is performed. When Origin is not set, or is set to any other origin than my specific site, the Cloudflare will respect the single-url purge and start serving a new response.

In fact I can get two different responses out of Cloudflare at the same time: a very old (stale/purged) response when Origin is set to my specific site, and at the same time I get newer cached (valid) response when Origin is set to anything else, or not set.

Try this (I don’t really want to post my actual API):

curl --location 'https://api.my.site/path/to/api' \
--header 'Origin: https://my.site'

The response is:

Date: Thu, 03 Aug 2023 04:29:38 GMT
CF-Cache-Status: HIT
Age: 25511
Last-Modified: Wed, 02 Aug 2023 21:24:27 GMT
Server: cloudflare

And then this gets a newer response (actually any :

curl --location 'https://api.my.site/path/to/api' 

Response:

Date: Thu, 03 Aug 2023 04:35:39 GMT
CF-Cache-Status: HIT
Age: 1121
Last-Modified: Thu, 03 Aug 2023 04:16:58 GMT
Server: cloudflare

Why does setting Origin to my specific site (different hostname than the API) cause a stale response to be served even after a purge, until Purge Everyhing is performed?

There is a “Cache Everything” page rule for api.my.site/*, and no other page or cache rule configured.

There’s a Vary: Origin, Accept-Encoding in the response which explains why a different response is served for different Origin, but it doesn’t explain why all other values of Origin respect the purge, but Origin: https://my.site never get’s purged.

Ok, I’m going to answer my own question here, but I feel like this a caveat that may go unrealised by a lot of people, at least initially.

From the purge docs ​Purge by single-file · Cloudflare Cache (CDN) docs

A single-file purge performed through your Cloudflare dashboard does not clear objects that contain any of the following:

  • Custom cache keys
  • Origin header
  • …and more…

You can purge objects with these characteristics using an API call to (purge files by URL). In the data/header section of the API call, you must include all headers and cache keys contained in the cached resource, along with their matching values.

So, in short, any subrequests from a site (e.g. with the Fetch API) will naturally have the Origin header set by the browser, and therefore Cloudflare requires you to specify the exact combination of headers and URLs when purging. If you don’t specify the header value in the purge request, then only requests without the Origin header will get purged.

So you’ll need to know all possible combinations of header values and purge them all. Quite hard if you are developing an API used by any number of sites.

Your only other option it seems is to pay (who knows how much?) for the Enterprise plan which enables purging by URL prefix. I’m all for paying for the services that I use… but an enterprise level plan just solely for cache purge by URL that other CDNs support out of the box was a bit of a shock.

2 Likes

Just adding another workaround for this limitation of not being able to purge the cache without knowing all possible originating sites (all possible values of the Origin header, which forms part of the cache key along with the URL):

  • Proxy the server in question with a Worker (that is, the Worker serves all routes and performs a subrequest to the actual origin server).
  • When performing the subrequest to the server from inside the Worker, make sure to remove the origin header from the subrequest
  • Perhaps also set { cf: { cacheEverything: true } } on the fetch() call if appropriate to enable cacing of the subrequest (if the URL doesn’t have a file extension)
  • Perform cache purge on the origin URL (the subrequest URL) not the URL proxied by the Worker

In this way, the originating site (Origin header) does not form part of the cache and you can easily purge by URL without knowing all originating sites. Obviously now you pay for the Worker usage and quota for every request (whether the subrequest is cached or not).

please refer to this post for a concrete example of how to purge a speific origin header Why after purge_cache , the data keep old when with origin - #4 by zaidoon

In addition, if you don’t need the origin header to be included in the cache key, you can use a transform rule that will remove this header (this is free):


2 Likes

I just tired the solution you proposed and removed the origin header from the request headers using transform rule but after that I started receiving CORS error “as been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource”

Am I missing something?