When No Cache means Cache - Fun with Azure Front Door

Posted 21 June 2021, 17:30 | by | Perma-link

Azure Front Door is a great product, that has only improved since it's initial release. At a high level it wraps three core services that most websites can benefit from: Caching (CDN), Routing (both simple Traffic Manager style and more complex rules based) and Firewalls (WAF). It also works really well behind the bigger full featured CDN offerings when you need more complex caching rules. It's easy to lock down an App Service to the Front Door infrastructure, providing you with the benefits of Web Application Firewall and failover if that's what you need.

However we recently had the following issue with Azure Front Door, caching and cookies: For sites where Front Door has Caching enabled, Front Door was stripping the set-cookie header from responses. This was causing form validation to fail when the Request Verification Token cookie wasn't returned in the POST.

Based on the documentation around cache expiration:

Cache-Control response headers that indicate that the response won't be cached such as Cache-Control: private, Cache-Control: no-cache, and Cache-Control: no-store are honoured.

We had ensured that our pages were sending Cache-Control: no-cache and were seeing x-cache: TCP_MISS on the responses so we thought we were good, but the cookies weren't being set. Checking the origin, they were being set fine, and disabling caching in Front Door resulted in them being set as expected as well, but none of the site was then cached.

Here's where the limitations of Front Door, compared to Azure Premium CDN show - the new Rules engine in Front Door allows you to modify responses, routing and caching behaviour, but only based on the incoming request (Azure Premium from Verizon CDN rules engine also allows you to modify those things based on the incoming request as well as the response from the server). So as an initial work around we disabled caching, and then enabled it with a rule for requests that included a file extension:

Front Door Rules Engine

In psuedo-code:

IF Condition: "Request Path"
   Operator: "Contains"
   Value: "." Transform: "To Lowercase"
THEN Action: "Routing Configuration"
     Route Type: "Forward" "Backend Pool"
     Backend Pool: // Update as needed
     Forwarding Protocol: // Update as needed
     URL Rewrite: // Update as needed
     Caching: "Enabled"
     Cache behaviour: "Cache every unique URL" // We want cache busting query strings to work
     Dynamic compression: "Enabled"
     Use default cache duration: "Yes"

This gave us a level of caching for static content (CSS, JS, images, etc.) but still meant that cacheable pages were not being cached.

After a bit of to and fro with the very helpful support team, it was pointed out that the HTTP specification has this to say about Cache Headers:

The "no-cache" response directive indicates that the response MUST NOT be used to satisfy a subsequent request without successful validation on the origin server

And the MDN documentation spells it out even plainer:

no-cache The response may be stored by any cache, even if the response is normally non-cacheable. However, the stored response MUST always go through validation with the origin server first before using it.

Because pages with a response of "no-cache" may actually be cached, Front Door automatically strips the set-cookie header from the response, ensuring that the page can be cached and other users don't share the set-cookie header.

What we needed to do was use the Cache-Control: no-store on those pages, which results in a truly non-cacheable page, and then Front Door lets the cookies through.

This basically meant changing our code from:

Response.Cache.SetCacheability(HttpCacheability.NoCache);

To:

Response.Cache.SetNoStore();

Your page will then emit a cache control header with private, no-store and an expires header set to -1. While this does help you fall into the pit of success, it's a little tedious that No Store doesn't exist on the HttpCacheability enum, and that attempting to set the cacheability manually to no-store results in an exception.

Filed under: Azure