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

  1. Enable Flag Block and place a "Flag block" for any flag on a cacheable
    route (e.g. a node canonical page).
  2. Enable the Dynamic Page Cache.
  3. Visit the page as a user who can see the flag link and inspect the
    response headers.
  4. Observe X-Drupal-Dynamic-Cache: UNCACHEABLE (poor cacheability)
    and X-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: a route cache 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 returns NULL cleanly.

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

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

yannickoo created an issue. See original summary.

yannickoo’s picture

Issue summary: View changes
yannickoo’s picture

Issue summary: View changes

yannickoo’s picture

Status: Active » Needs review