1

I have a form which is displayed in the frontend, through which anonymous users can create nodes. For guest users, the pages are cached and with them apparently some part of the form. We recently noticed that when a guest user opens the form and a second guest user also opens the form before the first user has submitted the form, then the second form can no longer be submitted. It's not a race condition, it doesn't matter when the second form is submitted or even who of the two users submits first. The second user gets a "duplicate entry" error for the UUID field of the node.

I dug around a bit in the code creating the form, and there is a part where a new node of the correct bundle is created, before the form and form_state objects are created. It seems this new node or the whole form_state are then cached. The problem goes away if I add a pre_save hook for the node and set a new UUID there.

Now this doesn't seem like an unusual scenario, so my question is: Is this an expected behavior or should frontend node forms be cacheable and we somehow brought this on ourselves with some kind of mis-configuration?

The reason I'm not sure is that there are a lot of people asking how to disable the caching for pages with a form. I didn't see much info on whether forms like this should be cacheable, though. I did find this question where Berdir answered that frontend forms should be cacheable. The old bug for the user registration sounds exactly like what is happening for us. The answer is six years old, though, so is this still the correct answer in 2023?

We are on Drupal 9.5.11 (going to upgrade to 10 soon). I can post code examples tomorrow, if it seems that this is some kind of misconfiguration. I mean, we could simply go with resetting the UUID, but it seems hacky to me. Something like this shouldn't really be necessary, and I'm a bit afraid that this might have other, harder to spot side-effects.

Update: The code base is some years old and more complex than that, but boiled down to a simple example it looks like this. We have a block that we place on pages via the Layout Builder:

namespace Drupal\jf_test\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\jf_test\Form\TestContentForm;

/**
 * A test block.
 *
 * @Block(
 *    id = "test_content_form_block",
 *    label = @Translation("Test content form block"),
 *    admin_label = @Translation("Embed a content form for creating articles"),
 *  )
 */
class TestContentFormBlock extends BlockBase {

  public function build(): array {
    return TestContentForm::make();
  }

  public static function create(): array {
    return \Drupal::service('plugin.manager.block')
      ->createInstance('test_content_form_block')
      ->build();
  }

  public function getCacheMaxAge(): int {
    return 0;
  }

}

And the form class:

namespace Drupal\jf_test\Form;

use Drupal\Core\Form\FormState;

class TestContentForm {

  public static function make(): array {
    return (new TestContentForm())->build();
  }

  public function build(): array {
    // Create a new node entity of the correct bundle.
    $node = \Drupal::entityTypeManager()
      ->getStorage('node')
      ->create(['type' => 'article']);

    // Build the form.
    $form = \Drupal::entityTypeManager()
      ->getFormObject($node->getEntityTypeId(), 'default')
      ->setEntity($node);
    $form_state = new FormState();
    $form_state->setValue('is_embedded', TRUE);
    // Workaround for "The same form twice on one page with different arguments
    // may process the wrong form when submitted"
    // @see https://www.drupal.org/project/drupal/issues/2821852
    $form_state->setRequestMethod('POST');
    $form_state->setCached(FALSE);

    return \Drupal::formBuilder()->buildForm($form, $form_state);
  }

}

I just ran this in a fresh Drupal 10.1 installation, and of course it worked without a problem. I also ran it in our Drupal 9.5.11 instalation, and it also worked there. So I guess I have to find out what's different about our specific node types we are trying to create.

7
  • 1
    By default only the rendered output of forms is cached, not the form+entity object. Did you change the defaults?
    – 4uk4
    Nov 5, 2023 at 18:50
  • 1
    Perhaps if you showed the code we could give an answer.
    – cilefen
    Nov 6, 2023 at 0:13
  • 1
    Thanks for looking at my question. I added some code examples. "Have you changed the defaults" is not an easy to answer question with a large and inherited code base. Since I now know that this is supposed to work and indeed does work for simple node types, I will try to rework the example code to see what the difference is to the real-world code. Nov 6, 2023 at 10:56
  • $form_state->setCached(FALSE); sets the default value, so it has no effect. But since you are saying the example works fine, you have to look in the non-working code where it sets cached to TRUE.
    – 4uk4
    Nov 6, 2023 at 12:21
  • max-age does not bubble drupal.org/project/drupal/issues/2352009
    – cilefen
    Nov 6, 2023 at 13:16

1 Answer 1

0

"Actually, TRUE is the default value ..." No, I meant the default value of FormState::$cache is FALSE in the $form_state object. It should not be set to TRUE, which calling setCached() does without argument (as commented by @JohannesFreudendahl because of the default in the method).

The caching behavior of the form object is calculated automatically depending on the state of the form and Drupal does the necessary steps to connect a unique ID to cached form objects. For entity forms this includes the entity instance which is stored in $form_state. Every new entity instance has a unique UUID.

2
  • Sorry, I thought you meant the default value of the setCached method argument. This absolutely fixes the duplicate UUID issue. It does open up the bug where with two forms on the same page the first form is always submitted. I'm looking into how to better fix this. Nov 6, 2023 at 13:59
  • 1
    You have to add a unique sub_id. For example the block ID where the form is placed in. See drupal.stackexchange.com/questions/253971/…
    – 4uk4
    Nov 6, 2023 at 14:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.