Problem/Motivation

A translated read labels itself with Content-Language on the first request and omits the header entirely on every subsequent identical request. The body is always correct; only the label disappears, and it disappears exactly when the dynamic page cache starts serving the response.

The header is set from a request attribute that only the controller populates. On a dynamic page cache hit the controller never runs, so the attribute is absent and the listener does nothing. The stored copy has no header either, because the cache stores at RESPONSE priority 7 while the header is stamped at priority -10.

Two shipped requirements depend on that header. translation-endpoints requires the response to set Content-Language to the returned translation, and language-fallback-reads requires a fallback read to be self-describing: "the Content-Language response header and the resource's langcode attribute MUST state the language actually served".

Fallback mode is where this hurts most. With ?langCode=fr&includeFallback=1 the served language is routinely not the requested one, so the header is the only thing telling a client what it actually received, and it is precisely what vanishes once the response is cached.

The exposure grew when #3613686 replaced the blanket page-cache kill switch with a narrower one, letting the dynamic page cache serve translated reads as intended.

Steps to reproduce

  1. Site with en (default) and fr, and an article translated into fr.
  2. GET /jsonapi/node/article/{uuid}?langCode=fr200, X-Drupal-Dynamic-Cache: MISS, Content-Language: fr.
  3. Repeat the identical request → 200, X-Drupal-Dynamic-Cache: HIT, and no Content-Language header at all.
  4. Same with ?langCode=fr&includeFallback=1 against an entity that has no fr translation: the cold response is correctly labelled en, the warm one is unlabelled.

Proposed resolution

Keep one listener, and make it work whether or not the controller ran.

The obvious fix, stamping the header before the dynamic page cache stores the response at priority 7, does not work, and it is worth recording why. Core's FinishResponseSubscriber::onRespond() sets Content-Language from the negotiated interface language unconditionally at its default priority 0 (Symfony's HeaderBag::set() defaults to replacing), and HttpKernel re-dispatches the RESPONSE event for a cache hit exactly as for a fresh response. Anything stamped above priority 0 is therefore overwritten on every request, warm or cold. Only a listener below 0 can win.

So the existing listener at -10 keeps its priority and gains a fallback: it prefers the controller-set attribute, and when that is absent it derives the langcode from the served document's own data.attributes.langcode. The document is used rather than the request's langCode parameter deliberately, because in fallback mode the requested language is not the served one, and the header must state what was served.

That fallback is scoped to the module's own surfaces, the JSON:API route flag or the jsonapi_menu_items route name, so it never touches other responses. Without the guard every cacheable 200 on the site, HTML pages included, would pay a full-body json_decode() looking for a key that cannot be there. This is the same scoping lesson as #3613670, and the route-name check is required in addition to the flag because jsonapi_menu_items' route carries jsonapi_resources' _jsonapi_resource flag rather than core's _is_jsonapi.

Included in the same change, since it is the same two response-rewriting listeners: both setContent(Json::encode(...)) call sites are guarded with is_string(). Json::encode() returns FALSE on failure, and under declare(strict_types=1) passing that to setContent() is a TypeError, turning a degraded response into a 500.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

lauriii created an issue. See original summary.

  • lauriii committed 1b85da0d on 1.0.x
    fix: #3614383 The Content-Language response header is lost on every...
lauriii’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.