Problem/Motivation

Starting in Webform 6.3.1, saving a webform as a draft fails with a LogicException whenever the form contains a managed file upload. Submitting the same form normally (without saving as a draft) works without problems. This works correctly in Webform 6.3.0.

The full error message is:

LogicException: Form errors cannot be set after form validation has finished. in Drupal\Core\Form\FormState->setErrorByName() (line 1119 of /web/core/lib/Drupal/Core/Form/FormState.php).

#0 -/web/core/lib/Drupal/Core/Form/FormState.php(1156): Drupal\Core\Form\FormState->setErrorByName()
#1 -/web/modules/contrib/webform/src/Plugin/WebformElement/WebformManagedFileBase.php(915): Drupal\Core\Form\FormState->setError()
#2 [internal function]: Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::valueCallback()
#3 -/web/core/lib/Drupal/Core/Form/FormBuilder.php(1264): call_user_func_array()
#4 -/web/core/lib/Drupal/Core/Form/FormBuilder.php(1004): Drupal\Core\Form\FormBuilder->handleInputElement()
#5 -/web/core/lib/Drupal/Core/Form/FormBuilder.php(1074): Drupal\Core\Form\FormBuilder->doBuildForm()
#6 -/web/core/lib/Drupal/Core/Form/FormBuilder.php(1074): Drupal\Core\Form\FormBuilder->doBuildForm()
#7 -/web/core/lib/Drupal/Core/Form/FormBuilder.php(449): Drupal\Core\Form\FormBuilder->doBuildForm()
#8 -/web/core/lib/Drupal/Core/Form/FormBuilder.php(633): Drupal\Core\Form\FormBuilder->rebuildForm()
#9 -/web/core/lib/Drupal/Core/Form/FormBuilder.php(326): Drupal\Core\Form\FormBuilder->processForm()
#10 -/web/core/lib/Drupal/Core/Entity/EntityFormBuilder.php(48): Drupal\Core\Form\FormBuilder->buildForm()
#11 -/web/modules/contrib/webform/src/Entity/Webform.php(1243): Drupal\Core\Entity\EntityFormBuilder->getForm()
#12 -/web/modules/contrib/webform/src/Controller/WebformEntityController.php(97): Drupal\webform\Entity\Webform->getSubmissionForm()
#13 [internal function]: Drupal\webform\Controller\WebformEntityController->addForm()
#14 -/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array()
#15 -/web/core/lib/Drupal/Core/Render/Renderer.php(637): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
#16 -/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(121): Drupal\Core\Render\Renderer->executeInRenderContext()
#17 -/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext()
#18 -/vendor/symfony/http-kernel/HttpKernel.php(181): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
#19 -/vendor/symfony/http-kernel/HttpKernel.php(76): Symfony\Component\HttpKernel\HttpKernel->handleRaw()
#20 -/web/core/lib/Drupal/Core/StackMiddleware/Session.php(53): Symfony\Component\HttpKernel\HttpKernel->handle()
#21 -/web/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(48): Drupal\Core\StackMiddleware\Session->handle()
#22 -/web/core/lib/Drupal/Core/StackMiddleware/ContentLength.php(28): Drupal\Core\StackMiddleware\KernelPreHandle->handle()
#23 -/web/core/modules/big_pipe/src/StackMiddleware/ContentLength.php(32): Drupal\Core\StackMiddleware\ContentLength->handle()
#24 -/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(116): Drupal\big_pipe\StackMiddleware\ContentLength->handle()
#25 -/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(90): Drupal\page_cache\StackMiddleware\PageCache->pass()
#26 -/web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(48): Drupal\page_cache\StackMiddleware\PageCache->handle()
#27 -/web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(51): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle()
#28 -/web/core/lib/Drupal/Core/StackMiddleware/AjaxPageState.php(36): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle()
#29 -/web/core/lib/Drupal/Core/StackMiddleware/StackedHttpKernel.php(51): Drupal\Core\StackMiddleware\AjaxPageState->handle()
#30 -/web/core/lib/Drupal/Core/DrupalKernel.php(741): Drupal\Core\StackMiddleware\StackedHttpKernel->handle()
#31 -/web/index.php(19): Drupal\Core\DrupalKernel->handle()
#32 {main}

Steps to reproduce

Install Drupal 10.6.18 with Webform 6.3.1, with a private file system configured.
Create a webform with a file element and enable "Allow users to save a draft" under the form's Draft settings.
Log in as an authenticated user, upload a file, click Save Draft.
Result: the LogicException.

My yml source coude for the form is:

subject:
'#type': textfield
'#title': Subject
'#required': true
test_pdf:
'#type': document_file
'#title': 'Test pdf'
'#max_filesize': '1'
'#required': true

I already have a clean Drupal 10.6.18 / Webform 6.3.1 installation with a ready form to test if needed.

Issue fork webform-3626841

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

initsos created an issue. See original summary.

mukeshaddweb made their first commit to this issue’s fork.

liam morland’s picture

Version: 6.3.1 » 6.3.x-dev

What version did you upgrade from? Can you use git bisect to figure out which commit caused this?

initsos’s picture

Thanks for the suggestion.

I tested this on a staging copy:

- Webform 6.3.0: Save Draft with an uploaded file works.
- Webform 6.3.1: Save Draft with an uploaded file throws "LogicException: Form errors cannot be set after form validation has finished."

So this is a regression in 6.3.1. I can't run git bisect directly because I don't have Webform in a git checkout.

What I found from the stack trace:

- The exception comes from WebformManagedFileBase::valueCallback() (line 915), which calls FormState::setError() while FormBuilder::rebuildForm() runs after validation has completed. The full trace is in the issue description.
- My understanding is that the new file-reference check treats a file as invalid when !$file->isTemporary(). Saving a draft makes the file permanent, so the check fails when the form is rebuilt with the same input. This is my reading of the code and I haven't confirmed it in a debugger.
- I also saw "The uploaded file is invalid" appear when removing or adding files on a resumed draft. This may be the same cause, since those files are already saved.

I have a temporary local workaround in valueCallback(). It skips the check once validation is complete, and it trusts file IDs that are already saved on the submission (including rows of a multiple-value composite). On my site Save Draft works again, including custom_composite rows with webform_document_file elements. It skips a check in one code path, so it as a local stopgap and not a proposed fix. I can attach the diff if useful.

I used AI assistance to help analyze the stack trace and draft the workaround. I reviewed the code and tested it on my own site (the 6.3.0 versus 6.3.1 behavior and the workaround on my forms). I have not run the Webform test suite or done a security review of the workaround.

liam morland’s picture

Title: LogicException "Form errors cannot be set after form validation has finished" when saving a draft with an uploaded file » Regression: LogicException "Form errors cannot be set after form validation has finished" when saving a draft with an uploaded file
Issue summary: View changes
Issue tags: +Regression

It would be very helpful to figure out which commit caused the problem.

mingsong’s picture

liam morland’s picture

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.