Interestingly, one of the big CDN providers did have controls in their UI for explicitly allowing/disallowing Vary header entries but they disabled it for us at some point (e.g. it was still in the UI but greyed out). I assumed once we hit a certain level of traffic it was too computationally expensive? Ever since, I've avoided any kind of fancy header/response variance in APIs just in case I end up in the same situation. It is rarely a necessity. IIRC, the only thing they continued to support variance wise was gzip (e.g. content-encoding).
It's also worth noting they were extremely conservative with query parameters too. Also to reiterate, this was very high traffic and high volume with expectations of low latency, so probably not applicable to most people using CDNs for static website assets.
Seems strange; AFAIK in e.g. Varnish, Vary just means you get more "stuff" tacked onto the buffer that gets built from the request and then hashed to create the cache key.
And actually, come to think of it, if memory for N concurrent in-flight requests is the concern, then you don't even need an actual (dynamically allocated) buffer, either; presuming you're using a streaming hash, you can feed each constituent field directly into the hasher, with only the hasher's (probably stack-allocated) internal static buffer for blockwise hashing required. (Which you're gonna need regardless of whether you're doing any Vary-ing.)
So it's really just a question of how many CPU cycles are being spent hashing. And it's likely just going to be a difference between hashing 300 bytes (base request — hostname, path, headers that are always implicitly Varied upon) and 350 bytes (those things, plus whatever you explicitly Varied) per request. Doesn't seem like too much of a win... (especially when hardware-accelerated hashing ops operate on blocks anyway, such that you only get stepwise cost increases for every e.g. 128 bytes.) I wonder why they bothered?
If this had been a necessity then I would have probably dug into the request more deeply. It was a "pick your battles" kind of thing. Extremely low cost on our side to change, no reason to bother if they claimed it would decrease problems on their side.
I think the best suggestion was in another thread by @johncolanduoni where he pointed out the difficulty of storing, distributing and retrieving the metadata per-resource that would be necessary for each PoP to correctly determine the Vary requirements at request time.
I think that is probably why some CDNs have a single configuration for any HTTP headers you want to vary on (e.g. Cloudfront allows you to specify a global configuration for a distribution that takes into account specific headers). This avoids the problem of both per-resource and inter-datacenter consistency that relying on the Vary header might cause.
> The Vary HTTP response header describes the parts of the request message aside from the method and URL that influenced the content of the response it occurs in.
In other words, if the server backend has a resource with representations that Vary on header values {A,B,C,D}; and one client sends req headers {A,B} — then by the standard they should only be told `Vary: A, B`; while if another client sends req headers {C,D}, then they should only be told `Vary: C, D`. The client should not be told in the Vary response header, about request headers they didn't send.
So it's not just that you can wait for the backend to send a `Vary` response header, and then medium-term cache the value of that header in the cache-policy metadata for the cache key. Instead, on each response, you need to
1. collect any additional Vary fields from the response and add them to your cache-policy Vary set; and
2. have some idea of what the "default header value" would be, to use as a fallback value when computing the cache key, for each header that isn't sent, when it's part of the active Vary set, so that you can dedup requests that explicitly send the header with value X, with request that don't send the header at all but where the default value would be X.
3. Also, ideally, you have a library of normalization transforms for the value of each header used in Vary, to decrease cardinality (the approach of this taking up the majority of the page space on the Varnish docs for Vary: https://varnish-cache.org/docs/3.0/tutorial/vary.html)
And the knowledge required to do all this correctly is really... not knowledge that a middlebox has any good way of acquiring.
This is starting to feel like a design smell in HTTP. Maybe zero-RTT content negotiation is misguided?
What if we instead did content negotiation like this (which — correct me if I'm wrong — would be a mostly ecosystem-backward-compatible change):
- if a resource negotiates, then by default, the server will send a 406 error response for all attempts at retrieving the resource. It sends this because the client itself needs to prove it knows what fields the resource varies on — and, of course, it doesn't know (yet), because nobody's told it yet. This 406 response contains a novel "Should-Vary" response header, informing the client of what it should be sending.
- to actually fetch a resource representation, the client is then expected to make the same request again, but this time, sending an Expect-Vary request header, the value of which matches the Should-Vary header value it saw from the server. Note that unlike with the Vary response header, this Expect-Vary request header should include header names that aren't part of the set of headers it's sending. (And/or, this list should force the client to emit explicit headers with its choice of implicit-default values for any headers listed in its own Expect-Vary header.)
- Upon receiving a request for a resource that negotiates, where the request has the Expect-Vary header set, the server will first verify that the Expect-Vary header value matches the Should-Vary value it would return for the resource, and either matches or is a superset of the Vary value it would compute as the response header given 1. the resource and 2. the rest of the received request. If this verification fails, that's a 406 again, sending Should-Vary again. If the verification passes, and the rest of the HTTP state workflow goes through, then you get a 2XX response. This 2xx response has the old Vary header as part of the response — but it now only exists for ecosystem back-compat.
- If a client thinks it knows the right Expect-Vary header to send, it can try sending it as a request header in the initial request. After all, the worst that can happen is the same 406 error it'd get otherwise. As well, the observed Should-Vary response header value of a resource can be cached basically indefinitely by the browser in its Expect-Vary cache, since the next time it changes for a resource, the browser will try its cached value for Expect-Vary in the request, and get a 406 response that tells it the new Expect-Vary value it should be using instead.
- Optionally, for efficiency, there could be introduced an Others-Should-Vary response header with the value being a path pattern (similar to a Set-Cookie Path field), which specifies other path prefixes for the host that should all be assumed by default to have the same Should-Vary header value as the response does. Potentially, a Should-Vary response header could also be sent in OPTIONS responses, to set a fallback assumed Vary value for the HTTP origin as a whole. (Clients are already requesting OPTIONS for CORS anyway; may as well give them some more useful information while we've got them on the line.)
With this design, middleboxes could safely trust the client's Expect-Vary header and use it to build the cache key — as long as 406 responses aren't cached.
Something for an RFC, maybe?
Some effort to clean up the useless and duplicate features would be good.
If you're talking to an API, you should know what that API can produce.
And if you're talking to a webpage, this isn't an issue.
Well as you're quite interested in APIs: the accept header is a fantastic way to version a REST API, and even has a built in way to handle clients that can talk to multiple versions of the same API.
Your app could request just 'application/vnd.foo.v1+json' from an API and if it's a centralised service that may be fine.
If the API your app talks to is something that's deployed to customers, or rolls out to regions incrementally or whatever, and thus can be at different versions, you need a way to handle that: the Accept header has you covered.
The Accept header does nothing that URLs can not.
URLs are opaque identifiers that only have meaning to the server that generated them. This makes any meaning implicit, where the accept header is an explicit and documented part of the interface. Maybe not much difference in practice for code you've interacted with, but that doesn't mean no difference at all.
If curl has trouble setting headers, that's a curl problem.
Of course you can have a unique URL for each format and avoid the accept header, just like you don't technically need more than GET and POST methods.
What kind of situation would you ever have a request like "Uh I want json the most but XML could work". Either the backend serves it in a format or not, just directly request what you want.
The accept header is passed along, so your server can respond however it wants for dynamic content/not cached by CDN.
E.g.
/fred.html - returns html
/fred.md - returns markdown
/fred.json - returns json
(I’m guessing that also — /fred - defaults to html
)Though he was a strict fundamentalist restafarian in other ways he held that this had the benefits of
1. Working
2. Being readily understood
I’ve seen worse ideas. Only downside is that the urls look a bit ugly.
https://example.com/#!/
https://example.com/#!/about/
https://example.com/#!/users/
Are all the same page as far as the browser concerns, and if you move from `/#!/` to `/#!/users/`, and reload the page, you still load the same page (i.e. `/`).But that was too ugly for URLs, so modern websites now use browser history APIs just so they can remove 3 characters from the URL and do stuff like:
https://example.com/
https://example.com/about/
https://example.com/users/
Sure, if you load `/` and move to `/about/` you don't load a new page because of browser history APIs; but if you then refresh, now you load a different, uncached page (`/about/`, instead of `/`) even if the HTML in the response is exactly the same as in `/`.Sure, the difference is not much, but to me it still seems like a waste when the response could have been cached already like in the first example.
Would back/forward buttons continue to work if we decided not use browser history APIs?
I could try with something like React, but that's too much for a quick test.
.replaceState() will replace current url without putting it in the history, so back won't work, while .pushState() does.
So they invented the history API's so SPA's could stop breaking some of the user expectations they were breaking.
https://developers.google.com/search/blog/2009/10/proposal-f...
If /fred returns HTML, then the URL is perfectly fine, since the only real consumer of fred.md or fred.json are automated systems/API clients, and they couldn't care less what the URL looks like, only that it's predictable.
github.com/[username].png
to get that username's profile picture. github.com/[username].keys
to get their ssh keys github.com/[username].gpg
to get their OpenPGP keys, too!What about any of that is "secret?"
This endpoint does not work.
Well, at least the appending .json to everything.
Together with that, you can also go for putting it in the `hash` part of the URL, if you want to keep it a secret from the server, as the server doesn't receive those parts, `/fred/#format=md`. In this case, it doesn't make much sense, but useful to know for other things, like keys and whatnot.
Server has no need for it.
Vary: Cookie, Authorization, Accept, Accept-Language
It's a documented decision that they've made.
> vary — Cloudflare does not consider vary values in caching decisions. Nevertheless, vary values are respected when Vary for images is configured and when the vary header is vary: accept-encoding.
When building a public scientific database I really want the URL identifying the item in the database to return the page for that item when I enter it in a browser but to return the appropriately structured data for that item when requested with "Accept: application/ld+json" or "application/rdf+xml" by a linked data library.
So it's unfortunate that there's no good way to support this with common CDNs.
Of course I always make it so that appending "?type=json" or "?type=xml" gets you the appropriate document.
That's not true. They do, but you have to add it to the Vary header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Va...
This way user-agents know that the Accept header is involved in the final form of the response. (As another example, Firefox also doesn't take Accept into consideration when caching locally by default, and it does with Varry: Accept)
Their documentation specifically mentions Vary as non-functional[1] outside a paid plan, and even then only for images[2].
[1] https://developers.cloudflare.com/cache/concepts/cache-contr...
[2] https://developers.cloudflare.com/cache/advanced-configurati...
A Vary field containing a list of field names has two purposes:
1. To inform cache recipients that they MUST NOT use this response to satisfy a later request unless the later request has the same values for the listed header fields as the original request (Section 4.1 of [CACHING]) or reuse of the response has been validated by the origin server. In other words, Vary expands the cache key required to match a new request to the stored cache entry.
[...]
When a cache receives a request that can be satisfied by a stored response and that stored response contains a Vary header field (Section 12.5.5 of [HTTP]), the cache MUST NOT use that stored response without revalidation unless all the presented request header fields nominated by that Vary field value match those fields in the original request (i.e., the request that caused the cached response to be stored).
So, Cloudfare doesn't actually implement HTTP/1.1: you can't just decide to ignore parts of the standard and still claim you implement it, that's only allowed for "SHOULD [NOT]" and "MAY" parts, not for the "MUST [NOT]" ones.Notice that this is the HTTP "Semantics" RFC, not the "core" one (Message and Syntax) which is the venerated RFC-7230 [1] which is indeed quite simple for such a widely used protocol.
This RFC only defines a handful of "header fields", almost all of which necessary for actually being able to "frame" (find the beginning and end, applying decoding if specified) the HTTP message : https://www.rfc-editor.org/rfc/rfc7230#section-8.1
Yes, I know (and personally experienced) that most clients forget to include "Vary" field, or don't know that it exists, or the problem that this header solves exists) but when they do include it, they actually mean it and rely on it being honoured, one way or another.
PS. By the way, 7230 is obsoleted, you're supposed to use 9112 now.
This means if you have an endpoint that returns HTML or JSON depending on the requested content type and you try to serve it from behind Cloudflare you risk serving cached JSON to HTML user agents or vice-versa.
I dropped the idea of supporting content negotiation from my Datasette project because of this.
(And because I personally don't like that kind of endpoint - I want to be able to know if a specific URL is going to return JSON or HTML).