Problem/Motivation

A /related/ read honours langCode when translating the resources it returns, but not when deciding which resources those are. It reads the relationship field off the host entity's default translation, so on a translatable entity reference field a client asking for French gets the English translation's targets.

The cause is a gap in this module's own fork. Core's EntityUuidConverter upcasts the routed entity through EntityRepository::getTranslationFromContext(), so a core controller receives an already-translated entity. This module deliberately removes that upcast, because it does its own request-driven language selection instead of relying on Drupal's language negotiation. EntityResource::getIndividual() compensates by resolving the requested translation itself. EntityResource::getRelated() never did: it calls $entity->get($field) on whatever the param converter produced, which is always the default translation.

The result is two endpoints disagreeing about the same data. These two requests read the same relationship, in the same language, and return different targets:

  • GET /jsonapi/node/article/{uuid}?include=field_related&langCode=fr → the French targets, because core's getIncludes() walks from the translation getIndividual() resolved.
  • GET /jsonapi/node/article/{uuid}/field_related?langCode=fr → the English targets.

This only bites where a reference field is translatable and its translations hold different values, which is an ordinary setup: "related articles" or a promoted-content field curated per language. Where every translation shares one value, the two paths agree and nothing is visibly wrong.

Steps to reproduce

  1. Site with en (default) and fr, article translation enabled.
  2. A translatable to-many entity_reference field on article, e.g. field_rel_multi.
  3. Two target articles, A and B, each with an en and an fr translation, so neither can be filtered out for language.
  4. A host article whose en translation references A and whose fr translation references B.
  5. GET /jsonapi/node/article/{host-uuid}/field_rel_multi?langCode=fr
  6. Observed: A, the English translation's target. Expected: B.

Captured verbatim from the characterization test written before the fix:

Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
 Array &0 [
-    0 => '48932953-e847-43cd-8714-925386b5d91c'   (the fr-referenced target)
+    0 => '2df3ed4c-3187-45ef-a6bf-c144b3b52148'   (the en-referenced target)
 ]

Both targets existing in both languages matters for the reproduction: it removes the existing per-target language filtering as an explanation, leaving the host translation as the only variable.

Proposed resolution

Resolve the host's translation before reading the relationship, exactly as getIndividual() already does.

  • getRelated() resolves the language selector first, then normalises the host with getUntranslated() and, when the host has the requested translation, reads the field from it. The $field_list assignment moves below that block.
  • In fallback mode a host lacking the requested translation is resolved through resolveViewableFallbackTranslation(), the same access-aware candidate walk an individual fallback read uses. The access results it consults are threaded onto the response's cacheability, so a cached body varies by the requesting user's access outcome rather than by URL alone.
  • The existing per-target language filtering, the to-one strict 404, and the meta.omitted reporting for excluded to-many targets are untouched. This changes which translation supplies the field, nothing about what happens to the targets afterwards.
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 ab3fb89b on 1.0.x
    fix: #3614409 /related/ reads the relationship from the host's default...
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.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.