The Vary HTTP Header is sent billions of HTTP requests every day. But it is never fulfilled its original version, many developers misinterpreted what it does or doesn’t even realise their web server sending it. With the coming of Keys, Variants and Client Hints, the varied request is getting the fresh start.
What is Vary?
Vary is start with a creative idea of how the web should work. In principle, a URL represents not a web page but a conceptual resource, like your bank statement. Suppose you want to see your bank statement: You will type bank.com send a Get request for the /statement. So far so good, but you didn’t define anything yet in the statement what format you want the statement in. This is the reason your browser will include something addition like Accept: text/html in your request. In theory, at least, this means you could say Accept: text/csv instead of getting the same resource in a varied format.
Because the same URL generate various responses based on the value to Accept header is important like this Vary: Accept.
You could read this as, “This response varied based on the value of Accept header of your request.”
This is fundamental doesn’t work with the today’s web. So it is called “ content negotiation” was a great idea, but it failed. This doesn’t mean that Vary is incompetent, though. A decent portion of the web pages you visit on the web carries a Vary header in the response – may be it websites containing too or you don’t know it. So, if the headers won’t work with content negotiation, why this method is still so important and how modern browsers deal with it. Let’s take a look.
Here you can read about Vary in the relation of content delivery networks CDN’s, those intermediary caches (Such as Fastly, CloudFront and Akamai) that you can put in between your users and servers. Browsers also need to understand and respond to Vary rules and the way, they defined is different from the way Vary is treated by the CDN’s. In this post, i have mentioned the rules of cache variation in the browser.
Use Cases For Varying in The Browser
As mentioned above, the traditional use of Vary is to perform content negotiation using the Accept, Accept-Language and Accept-Encoding headers, and historically, the first two methods have been failed miserably. Varying on Accept-Encoding to deliver Gzip and Brotli compressed responses, were supported, mostly works reasonably well, but all browsers now accept Gzip these days, so that isn’t very exciting.
How about some of these scenarios?
- We want to serve images that are the exact width of the user’s screen. If the user minimizes or maximise their browsers, We would download new images (Varying on Client Hints)
- If the user logged out, We want to avoid any cached pages they were logged in (Using a Cookie a key).
- Users browser support the WebP format for images should get WebP images; Otherwise, they should get JPEGs.
- When using a new browser window onto a standard screen width and refresh, they should get 1x images.
Caches All The Way Down
Unlike edge caches, which act as one gigantic cache shared by all the users, the browser is just for one user, but it has loads of different caches for various specific uses:
Some of these are quite new, and understanding exactly which cache the content is being loaded from a complex calculation is not well supported by the developer tools. Here’s what these caches do:
- Image Cache
This is a page-scoped cache that stores decoded image data, so that for example if you include the same image multiple times, the browser only needs to download and decode it once.
- Preload Cache
This is also a page-scoped and stores everything that has been preloaded in a Link header or a <link rel=”preload”> tag if the resource is ordinarily not cacheable. Like the image cache, the preload cache has been destroyed when the user navigates away from the page.
- HTTP Cache
This is the main cache that most people are familiar with. It is the only cache that pays attention to HTTP-level cache headers such a Cache-Control, and it combines all the caches with the browser’s own heuristic rules to understand whether to cache or for how long. It has the broadest scope which is shared by all the websites; so, if two irrelevant websites load the same asset (for example Google Analytics), they might share the same hit from the cache.
- HTTP/2 Push Cache
This cache sits with the connection, and it stores the objects that have been pushed from the server but have not been requested by any page that is using the same connection. It is scoped to pages using a particular connection, which is essentially the same a being scoped to single origin but it also moved permanently when the connection closes.
Of these, the HTTP cache and service cache has been well defined. As far the preload and image caches, some browsers might implement them as a single “memory cache” tied to the render of a particular navigation, but the mental model I’m describing here is still the prior way to think about the process. See the specification note on preload if you’re interested. In the case of H2 server push, discuss the fate of this cache remains active.
In order that request checks these caches before venturing out onto the network is important, because requesting something might pull it from an out of the layer of caching into inner one. For Example, if your HTTP/2 server pushing a style sheet with the <link rel=”preload”> tag, then the stylesheet will end up touching three browser caches. First, it will stay in the H2 push cache, waiting to be requested. When the browser is rendering the page and get the preload tag, it will pull the style sheet out of the push cache, through the HTTP cache (which might store it, depending on the style sheet’s, Cache-Control header), and save it in the preload cache.
Introducing Vary As A Validator
Ok, so what happens here when we may put in this situation and add Vary to the mix?
Unlike intermediary caches (Such a CDNs), browsers typically do not implement the capability to store the multiple variations per URL. The rationale for this is that things we typically use Vary for (prominently Accept-Language and Accept-Encoding) that don’t change frequently within the context of a single user. Accept-Encoding might but doesn’t change upon the browser upgrade and Accept-Language would most likely to change if you edit your operating system’s language locale settings. It also happens to be easier to implement Vary in this way, although some specification author believe in this could be a mistake.
It is not a huge loss most of the time for browsers to store only one variation, but it is important We don’t use variations accidentally that isn’t a valid anymore if the “varied on” data does happen to change.
The understanding is to treat Vary as a Validator, not a Keys. Browser compute cache keys in the normal way (essentially, using the URL), then if they score a hit they check the request satisfies any Vary rules that are baked into the cached response. If it doesn’t, then the browser treat the request as a miss on the cache, and it moves onto the next layer of cache or out to the network. When a fresh response is received, it will then overwrite the cached version, even though it’s technically different in variations.
Demonstrating Vary Behaviour
To demonstrate the way Vary is handled, you can test it here. This test will loads a range of different URLs, varying on different headers, and detects whether the request has hit the cache or not. In the test originally used as ResourceTiming for this, but for the more compatibility, in the test case is just ended measuring how long the request takes to complete (and intentionally added a one second delay to make difference in server-side response to understand clearly.)
Let’s have a look at each of the cache types and how Vary should work and whether it actually works like that. For each test, you can see either a (Hit or a Miss) and what exactly happens under the test case.
Preload cache is only supported in chrome browser where a preloaded cache is stored in memory cache until they are needed by the page. The responses also occupy the HTTP cache on their way to preload the cache, if they are an HTTP-cacheable. Because specifying request headers with the preload is impossible, and the preload cache lasts only as long as the page, testing is hard, but we can at least see the objects with a Vary header to get preloaded successfully.
Service Worker Cache API
Chrome and Firefox both support this type of cache and developing the service worker specification, the authors wanted to fix what they saw as broken implementations in the browser, to make Vary in the browsers work more like CDNs. which means the browser should store only one variation in HTTP cache, it is supposed to hold onto multiple variations in the Cache API. Firefox 54 does this correctly, whereas Chrome uses the same vary Validator logic that is used for HTTP cache (the bug is being tracked)
The main cache should understand Vary and does so consistently (as Validator) in all browsers. For much, much more on this, Read More “State of Browser Caching Revisiting”
HTTP/2 Push Cache
Vary should be observed, but in this method none of the browsers actually respects it and browsers will happily match and utilise pushed responses with the requests that carry random values in headers that the responses are assorted on.
The “304 (Not Modified)” Wrinkle
The HTTP “304 (Not Modified)” responses status is compelling.
“The server generating a 304 response must generate any of the following headers fields that would have been sent in a 200 (OK) response to the same request: Cache-Control, Content-Location, Date, Etag, Expires and Vary”
Why would 304 response return a Vary header? The plot condenses when you read about or supposed to do upon receiving a 304 response that contains those headers:
“If a stored response is selected to update, the cache must […] use other header fields provided in the 304 (Not Modified) response to replace all instances of the corresponding header fields in the stored response requests.”
Wait for what? So, if the 304’s Vary header is different from the one in the existing object cache, we’re supposed to update the cached object? But that might mean it no longer matches the request that has been made!
In that scenario, at a glance, the 304 seems to be telling you simultaneously that you can and cannot use the cached version. Of course the server same way didn’t want you to use the cached version, it would have to send a 200, not a 304; so, the cached version should definitely be used – but after applying the updates to it, it might not be used for future request identical to the one that actually occupied the cache in the first place.
Browser does seem to respect this but with the twist. They don’t just update the response headers but the request headers that pair with them, in order to guarantee that, post update, the cached response is a match for the current request. This will make a sense. The specification of this mentioned, so the browser vendors are free to do what they like; luckily, all browsers exhibit this same behaviour.
Client Hint is a feature that most significant things to happen Vary upon the browsers in a long time. Unlike Accept-Encoding and Accept-Language, Client Hints describe values that might well change regularly a user moves around your website, specifically the following:
Device Pixel Ratio, the pixel ratio of an image is vary, if the user has multiple screens.
Whether the user has enabled data saving mode.
Pixel Width of current Viewport
Pixel Screen Width
Not only these value changes for a single user, but the range of values for the width-related ones is large. So, we can use it totally Vary of these headers, but we risk reducing our cache efficiency or rendering ineffective caching.
The Key Header Proposal
Client Hints and other highly granular headers lend themselves to a proposal that Mark has been working on, named Key. Let’s take a look at it.
Key: Viewport-Width; div=50=
This tells that response varies based on the value of the Viewport-Width request header but rounded nearest at a multiple of 50 pixels!
Key: cookie; param=sessionAuth; param=flags
Adding this header into a response means that we’re varying on two specific cookies sessionAuth and flags. If both are not changed, we can reuse this response for a future request.
So the premier difference between Key and Vary are:
- Key allows varying on subfields within headers, which suddenly makes it feasible to vary on cookies because you can vary on only one cookie- this would be huge;
- Individual values can be bucketed into ranges, to increase the chance of a cache hit, particularly useful for varying on things such a Viewport width.
- All of the variation with the same URL must have the same key. So, if a cache receives a new response for the URL for which it already has some existing variants, and a new response’s Key header value doesn’t match the values on those existing variants, then all the variants must be eliminated from the cache.
The requirement for all the variations to have the same key recipe is that limiting, I’d like to see some kind of “early exit” option in the specification. This would enable you to manage things like Vary on authentication state and if logged in, also vary on preferences”.
The Variant Proposal
Key is a generic mechanism, but some headers have more complex rules for their values, and understanding those values’ semantics can help us to find automated ways of reducing cache variation. For example, imagine that two request comes in two different way Accept-Language values en-GB and en-US but although your website does have support for language variation, you only have one “English”. If we answer the request for US English and the response will be generated through a response cache in a CDN, then it can’t be reused for the UK English request. Because the Accept-Language would be different in this case and the cache is not smart enough to understand the context.
Right Now, variants are a very early draft, and because it is designed to help with Accept-Encoding and Accept-Language, it’s worth is rather limited to shared caches, such as CDN’s rather than browser caches. But it is nicely paired up with key and completes the picture for better control of cache variation.
There’s a lot to take in here, and while it can be interesting to understand how the browser works under the hood, there are also some simple things you can distil from it.
- Most of the browser handling Vary as a validator. If you want multiple variations to be cached, find a way to use different URL instead.
- Browsers ignore Vary for resources pushed using HTTP/2 server push, so don’t vary on anything you push.
- Browsers have tons of caches, and they will work in variation as it is requested. It’s worth trying to understand how your caching decisions impact performance in each one, especially in the context of Vary.
- Vary is not as useful as it could be, and key paired with the Client Hints is starting to change that. Follow along with the browser support to find out when you are started working on it.