Problem/Motivation
Every JSON:API collection response on the site pays a full-body json_decode() to look for a key that a collection document cannot contain.
ResponseSubscriber::onRespond() stamps the Content-Language response header. Its preferred source is a request attribute the controller sets, but on a dynamic page cache hit the controller never ran, so it falls back to reading data.attributes.langcode out of the serialized response body. That fallback landed with #3614383 and is correct; the problem is the gate in front of it, which is "is this a JSON:API route".
data.attributes.langcode only exists on a single-resource document. On a collection, data is a list, so the lookup can never match. The decode still runs first, on the largest documents this module serves, and the resulting array is discarded. On a decoupled site collections are the bulk of the traffic, and the cost is paid on warm cache hits too, which is precisely the path the fallback exists to serve.
The jsonapi_menu_items companion route is in the gate as well, and there the work is not merely wasted but redundant: its data is a list too, and MenuItemsLanguageSubscriber::onResponseLanguageHeader() runs afterwards at priority -15, against this listener's -10, and overwrites the header regardless. The route is in the guard purely to be ignored.
Steps to reproduce
No functional symptom; this is wasted work, visible by inspection or profiling.
- Enable
dynamic_page_cache. GET /jsonapi/node/articletwice, so the second is a cache hit.- On both requests,
ResponseSubscriber::onRespond()reachesJson::decode($response->getContent())and decodes the entire collection body. $body['data']['attributes']['langcode']isNULLevery time, because$body['data']is a list. The header is left to core'sFinishResponseSubscriber, exactly as it would have been had the listener returned early.
Proposed resolution
Gate the body-derivation on the two route shapes whose document can actually carry the key, instead of on the JSON:API route flag.
- The check becomes: route name starts with
jsonapi.and ends with.individualor.related. Those are the names\Drupal\jsonapi\Routing\Routes::getRouteName()generates for an individual resource and a related resource. - Routing runs at
KernelEvents::REQUESTpriority 32 and the dynamic page cache serves at 27, so_routeis populated even on the cache hit this fallback exists for. That is the same property the previous guard already relied on. - The
jsonapi_menu_itemsclause and theJSON_API_ROUTE_FLAG_KEYlookup both go, along with the now-unusedRoutesimport. The docblock is rewritten rather than left describing the old scope.
Issue fork jsonapi_multilingual-3614411
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
Comment #4
lauriii