Keep Accept-Language in Vary when no offered language is acceptable - #36
Open
namedgraph wants to merge 1 commit into
Open
Keep Accept-Language in Vary when no offered language is acceptable#36namedgraph wants to merge 1 commit into
namedgraph wants to merge 1 commit into
Conversation
…fered language is acceptable. Variant selection ran twice: request.selectVariant(variants), and on null a retry with request.selectVariant(removeLanguages(variants)). ContainerRequest.selectVariant assigns its varyValue field on every call, and ContainerResponse builds the Vary header from whatever the last call left there - skipping the header entirely when varyValue is null. So the retry published a Vary derived from a variant list with no language dimension, and only a request whose Accept-Language matched an offered language got the dimension at all. The entity had still been negotiated over Accept-Language and its content still depended on it, so a shared cache was free to store one language's representation and serve it to a client that asked for another. Reordering the two calls does not fix it: selecting over the full list last leaves varyValue null whenever nothing matches, and the response then carries no Vary at all - worse than an incomplete one. Verified, not assumed: with the calls reordered the lt request came back with no Vary header. So the language-neutral representations join the offer instead of replacing it, and one selection pass serves both purposes. The dimension stays in Vary because the list still declares languages, and a request accepting none of the offered languages still gets a representation because the list also offers language-neutral ones. A request that does match an offered language still selects the language-specific variant, so Content-Language and the language-specific ETag are unchanged - LocaleEntityTagTest.testLocales covers that and still passes. testVaryIncludesAcceptLanguage covers the three cases: a request accepting the offered language, one accepting only a language that is not offered, and a multi-entry header of the kind browsers actually send. It fails on the previous implementation - Vary comes back without the dimension - and passes on this one. Full suite: 61 tests, no failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Responseselected a variant twice:request.selectVariant(variants), and onnulla retry withrequest.selectVariant(removeLanguages(variants)).ContainerRequest.selectVariantassigns itsvaryValuefield on every call, andContainerResponsebuilds theVaryheader from whatever the last call left there — skipping the header entirely whenvaryValueisnull. The retry therefore published aVaryderived from a variant list with no language dimension, so only a request whoseAccept-Languagematched an offered language got the dimension at all.The entity was still negotiated over
Accept-Languageand its content still depended on it, so a shared cache was free to store one language's representation and serve it to a client that asked for another. Observed downstream in LinkedDataHub, where server-side XSLT renders over the full q-sorted acceptable-language list:Only an exact match on the one offered language was correct; every real browser header was not.
Why not just reorder the two calls
Selecting over the full list last leaves
varyValuenull whenever nothing matches, and the response then carries noVaryat all — worse than an incomplete one. Verified rather than assumed: with the calls reordered, theltrequest came back with noVaryheader.Fix
The language-neutral representations join the offer instead of replacing it, and a single selection pass serves both purposes:
Vary, because the list still declares languagesContent-Languageand the language-specificETagare unchangedTests
LocaleEntityTagTest.testVaryIncludesAcceptLanguagecovers three cases: a request accepting the offered language, one accepting only a language that is not offered, and a multi-entry header of the kind browsers actually send. It fails on the previous implementation —Varycomes back without the dimension — and passes on this one.The existing
testLocalesguardsContent-Languageand the language-specificETagagainst the neutral variant winning, and still passes. Full suite: 61 tests, no failures.🤖 Generated with Claude Code
https://claude.ai/code/session_0184W2B82P2wUBntUPie223L