Skip to content

Keep Accept-Language in Vary when no offered language is acceptable - #36

Open
namedgraph wants to merge 1 commit into
masterfrom
rf-vary-accept-language
Open

Keep Accept-Language in Vary when no offered language is acceptable#36
namedgraph wants to merge 1 commit into
masterfrom
rf-vary-accept-language

Conversation

@namedgraph

Copy link
Copy Markdown
Member

Problem

Response selected a variant 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. The retry therefore published a Vary derived from a variant list with no language dimension, so only a request whose Accept-Language matched an offered language got the dimension at all.

The entity was still 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. Observed downstream in LinkedDataHub, where server-side XSLT renders over the full q-sorted acceptable-language list:

Accept-Language: en                                 Vary: Accept-Charset,Accept,Accept-Language
Accept-Language: lt                                 Vary: Accept-Charset,Accept
Accept-Language: en,lt                              Vary: Accept-Charset,Accept
Accept-Language: en-US,en;q=0.9,da;q=0.8,lt;q=0.7   Vary: Accept-Charset,Accept

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 varyValue null whenever nothing matches, and the response then carries no Vary at all — worse than an incomplete one. Verified rather than assumed: with the calls reordered, the lt request came back with no Vary header.

Fix

The language-neutral representations join the offer instead of replacing it, and a single selection pass serves both purposes:

  • the dimension stays in Vary, because the list still declares languages
  • 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

Tests

LocaleEntityTagTest.testVaryIncludesAcceptLanguage covers 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.

The existing testLocales guards Content-Language and the language-specific ETag against the neutral variant winning, and still passes. Full suite: 61 tests, no failures.

🤖 Generated with Claude Code

https://claude.ai/code/session_0184W2B82P2wUBntUPie223L

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant