Problem

I ran into a problem with anonymous forms being cached. Turns out we have a problem here.

Here is the AI-generated analysis:

Why the CSRF Token Caching Problem Exists

How Standard Drupal Handles It

In standard Drupal, forms with CSRF tokens (the form_token field) have cache metadata with max-age: 0 that bubbles up through the render pipeline. The FormBuilder adds
#cache['max-age'] = 0 to the form render array. As Drupal renders the page, this metadata propagates upward through BubbleableMetadata, reaching the response level. The Dynamic
Page Cache and Page Cache respect this and don't cache the response.

What Goes Wrong in the Custom Elements Pipeline

The issue is in CustomElement::setSlotFromRenderArray() (line 262-276):

public function setSlotFromRenderArray($key, array $build, ...) {
$markup = $renderer->renderInIsolation($build); // line 269
// Add cache metadata from the render array.
$this->addCacheableDependency(
BubbleableMetadata::createFromRenderArray($build) // line 274
);
}

The problem:

1. renderInIsolation() renders the form to an HTML string. During rendering, child elements' cache metadata (including the form's max-age: 0 from CSRF tokens) bubbles into the
isolated render context, NOT back into $build['#cache'].
2. After rendering, BubbleableMetadata::createFromRenderArray($build) only reads $build['#cache'] — which has the pre-render cache metadata (whatever was on the top-level form
render array before rendering started). The bubbled-up max-age: 0 from the CSRF token element is lost in the isolated render context.
3. So the custom element gets cache metadata that doesn't include max-age: 0, and the response can be cached with stale CSRF tokens.

The Fix

The no-cache-authenticated patch works around this by explicitly setting max-age: 0 on the custom element for authenticated users. But the proper upstream fix would be to
capture the bubbled metadata from renderInIsolation() — Drupal's renderer returns it as part of the BubbleableMetadata in the render context, it just needs to be extracted and
merged into the custom element's cache metadata.

Steps to reproduce

Proposed resolution

Fix in our form integration, add test coverage.

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

fago created an issue. See original summary.

fago’s picture

Status: Active » Needs review

Add a MR with the help of claude code. It requires the related fix in custom_elements module, which ships with 3.3.4. That nicely shows the test-case to work, it first failed, after release, it becomes green. all ready to go!

  • fago committed 0b115612 on 1.x
    fix: #3575660 Form-building fails passing through cache metadata
    
    By:...
fago’s picture

Status: Needs review » 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.