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.

  1. Enable dynamic_page_cache.
  2. GET /jsonapi/node/article twice, so the second is a cache hit.
  3. On both requests, ResponseSubscriber::onRespond() reaches Json::decode($response->getContent()) and decodes the entire collection body.
  4. $body['data']['attributes']['langcode'] is NULL every time, because $body['data'] is a list. The header is left to core's FinishResponseSubscriber, 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 .individual or .related. Those are the names \Drupal\jsonapi\Routing\Routes::getRouteName() generates for an individual resource and a related resource.
  • Routing runs at KernelEvents::REQUEST priority 32 and the dynamic page cache serves at 27, so _route is populated even on the cache hit this fallback exists for. That is the same property the previous guard already relied on.
  • The jsonapi_menu_items clause and the JSON_API_ROUTE_FLAG_KEY lookup both go, along with the now-unused Routes import. The docblock is rewritten rather than left describing the old scope.
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 05e805e7 on 1.0.x
    task: #3614411 Every collection response json_decodes its whole body for...
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.