Problem/Motivation
FormBuilder::processForm() deletes a form's cache entry as soon as doSubmitForm() returns, provided $form_state->isCached() is true — regardless of whether the form was actually submitted:
if (!$form_state->isRebuilding() && !FormState::hasAnyErrors()) {
$submit_response = $this->formSubmitter->doSubmitForm($form, $form_state);
// If this form was cached, delete it from the cache after submission.
if ($form_state->isCached()) {
$this->deleteCache($form['#build_id']);
}
...
}
doSubmitForm() is a safe no-op when the resolved triggering element has '#executes_submit_callback' => FALSE (the default for #type => 'button', per Drupal\Core\Render\Element\Button) — but the cache-deletion check right after it is not guarded by the same condition. So any Ajax-only, non-submitting button that ends up as the triggering element on a cached, cacheable form causes that form's cache entry to be deleted even though nothing was submitted.
This is a documented inconsistency: the very next block in the same method has this comment, acknowledging the same gap:
// Typically, a submit handler calls $form_state->setRebuild(TRUE) when
// a fully executed form requires another step. However, for forms that
// have not been fully executed (e.g., AJAX submissions triggered by
// non-buttons), there is no submit handler to call setRebuild(). In
// that case, we also rebuild error-free, non-executed forms, regardless
// of $form_state->isRebuilding().
// @todo Simplify this logic; considering Ajax and non-HTML front-ends...
That later block correctly checks !$form_state->isExecuted() before deciding to rebuild — but by then the cache has already been deleted a few lines earlier.
Impact
On any complex, cached Ajax form where a non-executing Ajax button can end up resolved as the triggering element (for example, due to the fallback-to-first-button behavior described in #3174361: Media Library modal opens randomly on AJAX requests), the form's cache entry gets deleted even though the "submission" was a no-op. The form's next legitimate Ajax interaction then has nothing to retrieve from cache, forcing Drupal to silently rebuild the form completely from scratch. On a complex form (e.g. one combining Inline Entity Form and Media Library — see #3174361: Media Library modal opens randomly on AJAX requests), this fresh rebuild can lose in-progress widget state and again resolve the wrong triggering element, compounding the original bug.
Steps to reproduce
Hard to reproduce in isolation without the exact widget combination from #3174361: Media Library modal opens randomly on AJAX requests, since it depends on a non-executing Ajax button becoming the (wrongly) resolved triggering element on a form that is genuinely cached. See [#3174361]for a concrete, reproducible scenario.
Proposed resolution
Guard the cache-deletion call with the same isExecuted() check the code already uses a few lines later, so a no-op submission never deletes a cache entry that's still needed:
if ($form_state->isCached() && $form_state->isExecuted()) {
$this->deleteCache($form['#build_id']);
}| Comment | File | Size | Author |
|---|---|---|---|
| #2 | core-3617898-premature-cache-deletion.patch | 1.11 KB | matthiasm11 |
Comments
Comment #2
matthiasm11 commentedPatch in attachment, tested with Drupal 11.4.4.
Comment #3
matthiasm11 commentedComment #4
smustgrave commentedThank you for reporting. Fixes need to be in MRs though please
Also bugs typically need test coverage.
Thanks.