The issue was detected and resolved using AI. The functional impact of the underlying problem was fairly minimal for me, I only noticed error messages.
Problem/Motivation
The change record Recursion protection has moved to EntityViewBuilder (issue #2940605, Drupal 11.4) states that recursion is now tracked by a #pre_render/#post_render pair that sets and unsets "a tracking array entry indexed by the imploded cache keys of the render array for the entity". Crucially, this removed the old fixed render limit so the same entity may be rendered any number of times per page.
However, the shipped EntityViewBuilder::getRenderRecursionKey() does not use the render array's cache keys. It fabricates a key from the entity itself — entityType : entity_id : id : revisionId : [fiber_id] : langcode : view_mode — which is always non-empty, even when the entity is rendered in a non-cacheable context (where $build['#cache']['keys'] is empty).
Consequences:
- An entity rendered in a non-cacheable context should — per the documented design and per #3399945 ("a blank key should never be used to detect recursion") — produce an empty recursion key and therefore not be tracked. Instead it gets a non-empty, entity-identity key.
- Because the key depends only on entity identity + view mode (not on the render context/path), two independent or legitimately nested renders of the same entity in the same view mode collapse to the same key. The second render is then treated as recursion:
#printedis set toTRUE(its output is blanked) and aRecursive rendering attempt aborted for …warning is logged.
Real-world trigger: a content block used as a View's entity-area header that also appears among that view's rows (both in the same view mode, non-render-cacheable in that context). On an uncached page render, the row occurrence is blanked and the log fills with false warnings (observed ~6× on a homepage per language). This is finite, legitimate rendering — not an infinite loop.
Steps to reproduce
- On a multilingual or standard site, create a content block (
block_content) and enable a View with an entity-area (header/footer) handler that renders that block, plus rows that can include the same block, so the same block is rendered more than once in one request in a non-render-cacheable context. - Clear caches and load the page uncached (anonymous, cold render).
- Observe: the second occurrence of the block renders empty, and the log contains
User warning: Recursive rendering attempt aborted for block_content:entity_id:<id>:…:default. In progress: ….
Minimal essence: render the same entity twice in one request where $build['#cache']['keys'] is empty (non-cacheable render). The first render leaves a tracking entry keyed by entity id; the second is aborted.
Proposed resolution
Make getRenderRecursionKey() use the render array's own cache keys, exactly as the change record describes, and treat an empty key as "not tracked":
- Return
implode(':', $build['#cache']['keys'])(appending the current Fiber id for isolation, as today), or an empty string when there are no cache keys. - In
setRecursiveRenderProtection()andunsetRecursiveRenderProtection(), skip an empty recursion key (aligning with #3399945).
This keeps the mechanism limit-free: cache keys already encode entity type, id, view mode and langcode, so a genuine recursion re-enters with identical keys and is still aborted; independent/repeated renders continue to balance via #post_render and are never capped; and non-cacheable renders are simply not tracked (matching the documented intent).
Illustrative change to EntityViewBuilder::getRenderRecursionKey():
$keys = $build['#cache']['keys'] ?? []; if (!$keys) { return ''; } if ($fiber = \Fiber::getCurrent()) { $keys[] = 'fiber_id'; $keys[] = spl_object_id($fiber); } return implode(':', $keys);
Remaining tasks
- Confirm the intended behaviour with the #2940605 maintainers (the change record wording vs. the shipped implementation).
- Open an MR implementing the above.
- Add kernel test coverage: rendering the same entity twice in one request with an empty
#cache['keys']must render both occurrences and log no warning; a genuinely recursive render of a cacheable entity must still be aborted. - Decide whether to also fold in / dedupe with #3399945 (empty-key guard for paragraphs), which this resolves as a side effect.
User interface changes
None.
Introduced terminology
None.
API changes
None to public API. EntityViewBuilder::getRenderRecursionKey() is a protected, internal helper; only its key-generation strategy changes. Subclasses overriding it should adopt the cache-key strategy.
Data model changes
None.
Release notes snippet
Fixed a regression where entities rendered in a non-cacheable context (for example a content block shown by a View's entity-area handler that also appears among the view's rows) could be wrongly detected as recursive, blanking the repeated render and logging a "Recursive rendering attempt aborted" warning. Recursion protection now keys on the render array's cache keys as documented, and does not track renders that have no cache keys.
| Comment | File | Size | Author |
|---|
Issue fork drupal-3607889
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 #3
daften commentedComment #4
daften commentedI created an MR, also with the help of AI!
Comment #5
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #6
ryan-l-robinson commentedI have also encountered this error with some of my functional tests around custom entity types failing with error messages similar to the one in the description here.
I applied the proposed change here as a patch and so far it has fixed my tests. I don't know anything about the context of this code to be able to weigh in on whether this introduces any other problems.
Comment #7
daften commentedI addressed the failing PHPCS job.
The pipeline skipped the Kernel and Functional jobs; only the Unit jobs ran. So the recursion changes were not exercised by CI.
I thought about this some more and I added two kernel tests in EntityReferenceFormatterTest:
- testNonCacheableEntityRepeatedWithinOwnRenderTree: a non-cacheable entity rendered again as a descendant of its own render must not be flagged as recursion. This also covers the empty-key collision from #3399945.
- testCacheableEntityRecursionIsAborted: genuine recursion of a cacheable entity is still detected through its cache keys.
One open question remains. The change returns an empty key for non-cacheable entities, so they are no longer tracked. A non-cacheable self-reference is then no longer aborted. The existing testEntityFormatterRecursiveRendering asserts that it is. If non-cacheable protection should stay, a fallback key is needed instead of skipping. This will require input from a core maintainer imo.
The tests will partially fail, it's a step to have coverage for the scenario's failing first.
Generated with the help of an LLM.