Skip to content

Search API varies by Accept-Language without sending Vary or Content-Language #800

Description

@ddeboer

The GraphQL search surface varies its response by Accept-Language but sends neither Vary: Accept-Language nor Content-Language, so any shared cache or CDN in front of a deployment may serve one requester’s language to the next.

What varies by the header

parseAcceptLanguage in packages/search-api-graphql/src/handler.ts puts the parsed preference list on SearchContext, and it reaches two places:

  • Display orderingtoLanguageStrings(value, context.acceptLanguage, languageOrder) in build-schema.ts, documented at build-schema.ts:100 as “defaults to Accept-Language-first, und last”. name[0] therefore differs per requester.
  • Search and sortpackages/search/src/query.ts:30 selects the per-locale stemmed and sort fields from the same list, so result ranking and order differ too.

What is missing

Content-Language and Vary appear nowhere under packages/. Two consequences:

  1. Cache poisoning across languages. Without Vary: Accept-Language, an intermediary treats two requests differing only in that header as the same entry. A Dutch-first response can be served to an English-first client, or vice versa – silently, since nothing in the payload states which ordering was applied.
  2. The client cannot tell what it got. [0].language reveals the language of each individual value, but nothing reports which preference the server actually honoured for the response as a whole.

Suggested fix

Emit both from the handler:

  • Vary: Accept-Language on every search response (unconditionally – it describes the resource’s negotiation behaviour, not one response’s outcome).
  • Content-Language listing the languages actually present in the response. RFC 9110 §8.5 defines it as the languages of the intended audience and explicitly allows multiple values, so a mixed-language result page can be described honestly rather than collapsed to one tag.

Never fail negotiation: the current fallback behaviour (to und when no header is given, per build-schema.test.ts:813) is correct and should stay. Accept-Language is a preference here, not a precondition.

Context

Found while reviewing the NDE generic API specification against LDE and the NDE Stack. That specification mandates both headers (its Languages rules, items 4 and 6) while getting the negotiation semantics wrong in the other direction – it requires 406 when a requested language is unsupported, and forbids serving multiple languages in one response, neither of which survives contact with a corpus that is ~86% Dutch-only with routinely untagged proper names. The header emission is the half worth taking from it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions