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']);
}

Comments

matthiasm11 created an issue. See original summary.

matthiasm11’s picture

Status: Active » Needs review
Related issues: +#3174361: Media Library modal opens randomly on AJAX requests
StatusFileSize
new1.11 KB

Patch in attachment, tested with Drupal 11.4.4.

matthiasm11’s picture

Assigned: matthiasm11 » Unassigned
smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

Thank you for reporting. Fixes need to be in MRs though please

Also bugs typically need test coverage.

Thanks.