Caching per language
Goal: a product catalog answers in the caller’s language, and every caller who reads German
shares one cached German answer, whatever their browser puts in Accept-Language.
Partitioning on the raw header does not get you there. de-DE,de;q=0.9,en;q=0.8, de-AT and
de are three different strings, so they would be three cache entries holding the same German
text, and the number of entries grows with every browser and every language setting in the wild.
A vary rule can reduce the header to the
one part your origin actually uses.
The running example is shop-api, whose origin translates product names and category names.
It supports English and German and answers English for anything else.
What you need
Section titled “What you need”- A deployed service and its
gpilot.toml. - An origin that can read the language from a header GraphPilot sets, here
x-locale.
1. Derive the language
Section titled “1. Derive the language”[[cache.policy.vary]]name = "x-locale"value = "lower(split(split(split(header:accept-language, ',', 0), ';', 0), '-', 0)) || 'en'"pass_to_origin = "plain"Read the expression from the inside out, for de-AT,de;q=0.9,en;q=0.8:
| Step | Result |
|---|---|
split(header:accept-: the first language range | de-AT |
split(..., ';', 0): without a ;q= weight, if the first range has one | de-AT |
split(..., '-', 0): the primary language, without the region | de-AT becomes de |
lower(...): one spelling for DE and de | de |
|| 'en': the fallback when there is no header | en |
pass_to_origin = "plain" sends the result to your origin as x-locale: de. Answer from that
header, not from Accept-Language: the cache entry is keyed on x-locale, so an answer that
depended on anything else in Accept-Language would be served to callers it was not meant for.
The rule has no scope_only, so it partitions every cached response of the service, which is what
a translated catalog needs. Configuring it also claims the name: an x-locale header a client
sends itself is removed on the way in.
2. Deploy and send two requests
Section titled “2. Deploy and send two requests”Deploy with gpilot deploy, then ask for the same product with two different German browser
settings:
for lang in 'de-DE,de;q=0.9,en;q=0.8' 'de-AT'; do curl -sS -D- -o /dev/null "https://$GRAPHPILOT_HOST/graphql" \ -H 'content-type: application/json' \ -H "accept-language: $lang" \ -d '{"query":"query ProductName { product(id: \"p01\") { id name } }"}' \ | grep -i '^gp-cache:'donegp-cache: MISSgp-cache: HITBoth derive de, so the second request is served from the entry the first one stored. A third
request with accept-language: en-US derives en and is a MISS of its own.
3. Read it in Insights
Section titled “3. Read it in Insights”Open one of the requests in Insights. The Cache tab shows
Segmentation Vary, split by x-locale. By default the value is stored as a keyed
pseudonym (hmac-sha256: and 16 hex digits), marked pseudonymized, so the two German
requests show the same pseudonym and the English one another. Requests in this bucket lists
every request that read the German entry.
A language is not personal data, so you may prefer to read de in clear. That is
telemetry.vary_values = "clear", and it applies to every
vary rule of the service: switch it only if every other rule’s value is fine to store in clear.
Pitfalls
Section titled “Pitfalls”- Every language a client sends is its own entry. The rule collapses regions and weights, not
languages: a caller sending
xxgets anxxentry, answered in English by your origin. That stays bounded by the languages clients send, which is far fewer than the header strings. If you need a fixed set, let your client sendx-app-localefrom a list it controls and vary onlower(header:x-app-locale) || 'en'instead. - Only the first language range counts.
fr-CH,de;q=0.9derivesfr, even though your origin only speaks German and English. That is the right entry as long as your origin answersfrwith its fallback. - The rule splits everything. Responses that are the same in every language, such as prices served by a separate query, are stored once per language too. Keep untranslated data in its own operations if the hit rate matters.
- Only two languages? A
publicpredicate such asstarts_with(lower(header:accept-language), 'de')gives exactly two entries. Its value is not sent to your origin, so your origin has to pick the language fromAccept-Languagethe same way; seepublic. - A
gp-name does not work. GraphPilot removesgp-headers before your origin, sopass_to_origincould never deliver it. Use anx-name.