Problem/Motivation
FlagBlock::build() builds the flag link eagerly (it calls
flag.link_builder and returns the rendered link inline), and the
plugin overrides getCacheMaxAge() to return 0.
Flag action-link routes (flag.action_link_flag /
flag.action_link_unflag) require a CSRF token
(_csrf_token: TRUE). Generating a URL to a CSRF-protected route
forces max-age = 0 and a per-session cache context onto the render
tree. Because the block renders that link inline, this uncacheability
bubbles up to the whole page: every page the Flag Block appears
on becomes uncacheable in the Dynamic Page Cache
(X-Drupal-Dynamic-Cache: UNCACHEABLE (poor cacheability)), and the
block's own max-age 0 defeats the Internal Page Cache too.
Flag core itself does not have this problem for field-based links: in
flag_entity_view() it renders the link through a
#lazy_builder with #create_placeholder => TRUE, so the
per-session/CSRF part is isolated in a placeholder and the surrounding page
stays cacheable. The Flag Block should do the same.
Steps to reproduce
- Enable Flag Block and place a "Flag block" for any flag on a cacheable
route (e.g. a node canonical page). - Enable the Dynamic Page Cache.
- Visit the page as a user who can see the flag link and inspect the
response headers. - Observe
X-Drupal-Dynamic-Cache: UNCACHEABLE (poor cacheability)
andX-Drupal-Cache-Max-Age: 0— the whole page is uncacheable,
caused solely by the flag link inside the block.
Proposed resolution
Render the flag link through a lazy-builder placeholder instead of building it
inline, reusing flag's own trusted callback
(flag.link_builder:build), and drop the blanket
getCacheMaxAge() = 0:
- Return
['#lazy_builder' => ['flag.link_builder:build', [$entity_type_id, $entity_id, $flag_id, $view_mode]], '#create_placeholder' => TRUE]
so only the per-session link is uncacheable, mirroring
flag_entity_view(). - Remove the
getCacheMaxAge()override. Instead declare correct
cacheability for the block shell: aroutecache context (the block
resolves its entity from the current route) and the flag config entity as a
cacheable dependency. - Tighten the guards so an unset/invalid flag or a route without a flaggable
entity returnsNULLcleanly.
Result: X-Drupal-Dynamic-Cache goes MISS → HIT, the host page is
cacheable again, and the flag link still renders (via a BigPipe/placeholder),
identical to field-based flag links.
API changes
The FlagBlock constructor no longer takes the
flag.link_builder service (FlagLinkBuilderInterface),
as the link is now produced by the lazy-builder callback rather than built
eagerly. This only affects code that instantiates or subclasses
FlagBlock directly. The getCacheMaxAge() override is
also removed.
Issue fork flag_block-3608355
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 #2
yannickooComment #3
yannickooComment #5
yannickoo