Problem
-
This breaks the
HttpKernel
request/response flow, which means that neither cleanup-code in stack middlewares is executed. In most of the cases this is not a big problem and easy to resolve (worked on in #1497162: Eliminate the use of exit in core) but the early exit inFormBuilder
renders stack middlewares virtually unusable/buggy. - Limits our ability for KernelTestBase to test pages/controller output without http (aka BrowserKit driver for Mink) - which is a big step towards improving the performance of our test-framework
Task
-
Remove calls to
exit()
fromFormBuilder
that are breaking the request/response flow. Instead throw an exception with the desired response and catch it in a subscriber. This is an anti pattern and needs to be resolved properly in 9.x #2367555: Deprecate EnforcedResponseException support.
Occurences of exit in non-test code
$ git grep -E '(exit|die)(\([^\)]*\))?;' | grep -E -i -v 'test|script|vendor'
core/includes/batch.inc: // PHP did not die; remove the fallback output.
core/includes/bootstrap.inc: exit;
core/includes/errors.inc: exit;
core/includes/errors.inc: exit;
core/includes/errors.inc: exit;
core/includes/install.core.inc: exit;
core/includes/install.core.inc: exit;
core/includes/install.core.inc: exit;
core/install.php: exit;
core/lib/Drupal/Core/DrupalKernel.php: exit;
core/lib/Drupal/Core/Form/FormBuilder.php: exit;
core/lib/Drupal/Core/Form/FormBuilder.php: exit;
core/modules/toolbar/toolbar.module: exit;
All occurences in includes/
and install.php
are either trivial to resolve or do not pose a problem for the standard request/response cycle (#1497162: Eliminate the use of exit in core). The occurence in DrupalKernel
can be resolved by reworking the handlePageCache
method into a stack middleware (#2257695: [meta] Modernize the page cache). The one in toolbar
is to be removed by #2263981: Introduce a robust and extensible page cache-policy framework).
That means that only the FormBuilder
code absolutely needs to be reworked as part of this issue.
Comment | File | Size | Author |
---|---|---|---|
#62 | interdiff.txt | 4 KB | znerol |
#62 | 2230121-remove-exit-from-form-builder-62.patch | 25.38 KB | znerol |
#58 | interdiff.txt | 1.48 KB | znerol |
#58 | 2230121-remove-exit-from-form-builder-58.patch | 24.74 KB | znerol |
#56 | 2230121-remove-exit-from-form-builder-56.patch | 24.21 KB | znerol |
Comments
Comment #1
tim.plunkettWhile I'd love to see some improvements to FormBuilder, I'd like to note that it handles the request/response flow internally, and fires the terminate event just fine.
Comment #2
dawehnerHere is an idea how we could do it.
Comment #4
ParisLiakos CreditAttribution: ParisLiakos commentedComment #5
znerol CreditAttribution: znerol commentedReopening/rescoping this issue because the other one (#1497162: Eliminate the use of exit in core) has no solution for the
FormBuilder
(which is rather critical after the introduction of stackphp).Comment #6
znerol CreditAttribution: znerol commentedReroll.
Comment #7
znerol CreditAttribution: znerol commentedActually remove
exit
calls fromFormBuilder
(and also remove dependency on kernel), fix unit-test, rise priority of exception subscriber.Comment #10
znerol CreditAttribution: znerol commentedOh, fun. Exception thrown while exception is handled. This basically means that it is necessary to special case anything where something is rendered outside of
HttpKernel::handle()
.Remaining test-fails seem to have something to do with twig. I have no clue about this area.
Comment #11
znerol CreditAttribution: znerol commentedAlso examine every previous exception accessible via
Exception::getPrevious()
, this should resolve the twig issue.Comment #12
znerol CreditAttribution: znerol commentedComment #13
znerol CreditAttribution: znerol commentedSymfony
HttpKernel::handleException()
sets the response status code to 500 unless the exception handled implements theHttpExceptionInterface
. Therefore rebase the newHttpResponseException
onto the generic SymfonyHttpException
and make sure the response status code is propagated properly.Comment #17
znerol CreditAttribution: znerol commentedMore exception handler fun: If a custom error-page is configured, the HTTP status of the original error will potentially overrule the one of
HttpResponseException
. A possible solution is thatCustomPageExceptionHtmlSubscriber::makeSubrequest()
only enforces the status of the subrequest-response if it is smaller than 300.Comment #18
dawehnerAwesome work!!
What about using $response->isSuccessFul()? It would be great to document why we do this here.
Can we document why we iterate over the backtrace?
We should also document why we throw an exception here. This is not obvious to be honest.
Comment #19
znerol CreditAttribution: znerol commentedRe #18.1: This piece of code is in fact brittle. When a form decides to return a response with status=200, then the this will not work.
Before cleaning up the code, I'd like to try something different: Pass around a wrapped response and just unpack it from within a response-filter in the main-request. Like that it should be possible to properly propagate a response generated by a form in a subrequest to the main response.
Comment #20
znerol CreditAttribution: znerol commentedLet's be explicit about the use-case and rename
WrappedHttpResponse
toEnforcedResponse
andHttpResponseException
toEnforcedResponseException
and move them to theDrupal\Core\Form
namespace.Also rename
ExceptionResponseSubscriber
toEnforcedFormResponseSubscriber
.Comment #21
znerol CreditAttribution: znerol commentedPolish the documentation and revert #13. That is not necessary anymore because of the response-wrapping and filtering introduced in #19.
Comment #23
Crell CreditAttribution: Crell commentedThis is using exceptions as a flow-of-control tool, not as an error handling tool. That's very bad form. :-) That's why we went with the exit() in the first place, because that was less evil than using exceptions for program flow control.
Are we certain there's no alternative? If we are forced to go this route, it should be HEAVILY documented that it's a hack around fundamental design flaws in FAPI to discourage people from using it as an example.
Comment #24
tim.plunkettMaybe instead, you could explain the "fundamental design flaws", and your suggestion for what you would have designed instead. Then maybe we could implement that.
But just complaining again and again about unenumerated flaws is not helping anyone.
Comment #25
Crell CreditAttribution: Crell commentedIt's been over a year so I don't recall the details. In general terms, the problem is that FAPI has no way to return a value after processing a submitted form. I don't really recall who was working on that with me at the time, unfortunately, but the form build process had no way to return a response back out of the controller. It would have required heavily rewriting the form processing pipeline which no one had the stomach for at the time. It was very insistent on calling drupal_goto() as its last step. Turning that into a dedicated process exit point was the best we could do to ensure that response and terminate listeners still got called.
That said, with all of the refactoring of FAPI that you've done in the last few months it's possible that we could now find a way to return a RedirectResponse back from a processed form and use the standard pipeline, which would solve the issue here quite nicely. I've not been inside FAPI in a long time, though, so I don't know if that's just wishful thinking on my part. You've been in there more than I have so you can probably answer that far better than I could. But that's the original problem that prompted the dedicated process exit point.
Comment #26
ParisLiakos CreditAttribution: ParisLiakos commentedSee #1668866-128: Replace drupal_goto() with RedirectResponse and next..the problem is with nested forms with redirects.
I agree with Crell and #23, exceptions are used for error handling, this looks uglier than the exit call.
What is the problem with leaving it as is? Or just the fact that we want grep to return 0 results for exit()?nvm, just saw this is about middlewaresComment #27
BerdirYes, the problem is not within the Form component, it's how we have forms embedded in all kinds of places blocks and parts of pages and so on. Those can't pass the redirect through and they would have to add an is_array()/is response check everywhere.
The only way that could change I guess is if we'd route form submissions differently and directly to the form, similarly how we do it for standard ajax responses.
Comment #28
znerol CreditAttribution: znerol commentedIn order to fix the control flow of the form API one very fundamental step needs to be done first: Separate form-submission from rendering, i.e. the action of each form needs to point to a dedicated route.
Regrettably the call to
exit()
breaks stack middlewares.Comment #29
znerol CreditAttribution: znerol commentedNote that resolving this issue is necessary in order to #2229145: Register symfony session components in the DIC and inject the session service into the request object.
Comment #30
larowlanThis will also block a http less test base class, ie kernel only tests of controllers etc
Comment #31
catchI keep thinking there's an issue open for having form submits go to a dedicated route, but I can't find it. Not opening now - hoping someone else knows where that issue is. Let's open one if there's not.
Comment #32
Crell CreditAttribution: Crell commentedI know we discussed that back in Munich. Also the possibility of using a request listener that checked for a POST request (and various other things) regardless of the path being used. That would also be a viable option, I think.
Comment #33
znerol CreditAttribution: znerol commentedThat does not work because the API currently allows form builder methods to return a response (yes, when forms are rendered, not only when they are submitted). We could try to remove that "feature". Possibly affected code (with some false positives):
Comment #34
znerol CreditAttribution: znerol commentedGoing a bit through history (mostly #1668866: Replace drupal_goto() with RedirectResponse), turned up:
With #1668866-131: Replace drupal_goto() with RedirectResponse it was finally possible to make the form API work without drupal_goto. The problem we have now is that since then stack-middleware has entered the picture and I cannot think of any other way than throwing an exception to make this work with the current approach - except perhaps rewriting the form API entirely.
Regrettably I was not able to find the issue mentioned by @catch in #31.
Comment #35
catch@znerol so I'd personally be fine with us having that issue open, as major/critical and fixing post-beta. It'd be an API change but it may make this issue considerably easier to solve, as well as being a performance improvement on form submits.
Comment #36
znerol CreditAttribution: znerol commentedProblem is that this blocks #2229145: Register symfony session components in the DIC and inject the session service into the request object. I'm not sure about the priority over there, but given that it introduces the Symfony session service (i.e. the facade which anything operating on the session should use) I'd love to have this in place for the beta.
Comment #37
BerdirRelated: #2248297: Ensure routes are rebuilt when install modules
Comment #38
znerol CreditAttribution: znerol commentedReroll after #2248297: Ensure routes are rebuilt when install modules.
Comment #39
moshe weitzman CreditAttribution: moshe weitzman commentedWould anyone be willing to Assign this issue to themselves. It is a Critical so needs an owner. I'm a Base system maintainer and doing triage on critical bugs in this component.
I might get tomatoes thrown at me, but if the Critical problem here is breaking StackPHP, then we could consider rolling back StackPHP. Having forms POST to dedicated routes is a huge change at this point in the release cycle.
Comment #40
znerol CreditAttribution: znerol commentedFiled #2363189: Do not allow form builder functions to return Response objects.
Comment #41
dawehnerIs that owner nicety documented somewhere? https://www.drupal.org/node/2172049 says that assigning issues is a bad thing.
Well, the critical is that
exit()
is called fromFormBuilder
, no matter whether there is stack or not. Yes, callingexit()
is also problematic for stackphp,but it is not the problematic space.
Comment #42
Fabianx CreditAttribution: Fabianx commented#39: Regardless of stack or not calling the DrupalKernel and exit directly from some class is not a good idea as the DrupalKernel is an internal API used by the bootstrap process, not by modules.
#41: Assigning is probably not the right thing, but having one or several "owners" in the issue description that are both responsible and can be asked questions if someone wants to work on it, probably is.
#40: Lets postpone this shortly on #2362517: Improve default 403/404 exception HTML subscriber: don't duplicate the page render pipeline, use the routing system — add system.403 and system.404 routes and also see what really needs to happen here.
For just sending a 302 or such surely no response object but a more higher level API is needed, for all other cases I don't know why form builder would need to return a response early and the issue summary does not make this clear.
Comment #43
znerol CreditAttribution: znerol commentedNote @tim.plunkett independently came to the same solution (i.e. use exceptions for the control flow) in #2363189-20: Do not allow form builder functions to return Response objects. @Crell: Regrettably I cannot see any realistic alternatives to a) continue here and replace
exit
with exceptions or b) do what @moshe weitzman proposes in #39 and remove support for stacked kernels.I think the proposal in #32 is rather difficult to implement. The problem is that some form builder functions accept additional arguments. As a result it would be necessary to unconditionally cache any form along with its arguments (except those forms with a dedicated route). Even if we succeed to implement that, we'd still need a clean way to solve #2363189: Do not allow form builder functions to return Response objects.
Comment #44
Fabianx CreditAttribution: Fabianx commentedCan we get an issue summary update of why the FormBuilder needs to exit early in the first place?
Together with a proposed resolution?
Comment #45
moshe weitzman CreditAttribution: moshe weitzman commentedIn addition, the problem statement needs clarity. What is broken due to this bug. Note #1 where Tim says that the request/response is handled internally and the Terminate event is fired. If this about code cleanliness, it should not be Critical.
Comment #46
larowlanWorth nothing that things calling exit also limits our ability for KernelTestBase to test pages/controller output without http (aka BrowserKit driver for Mink) - which is a big step towards improving the performance of our test-framework
Comment #47
moshe weitzman CreditAttribution: moshe weitzman commentedGood point, @larowlan. Now I see that you mentioned that in #30 as well. Added to IS.
We don't have a suggestion better than the exception so I propose we proceed with latest patch - #38.
Comment #48
tim.plunkettSee also #2363189-20: Do not allow form builder functions to return Response objects, which is a more targeted approach and doesn't modify DefaultExceptionHtmlSubscriber directly, keeping it within the Form subsystem.
Comment #49
znerol CreditAttribution: znerol commented@tim.plunkett: #2363189-20: Do not allow form builder functions to return Response objects does not seem to resolve
exit()
during form submission.Comment #50
znerol CreditAttribution: znerol commentedReroll after #2362517: Improve default 403/404 exception HTML subscriber: don't duplicate the page render pipeline, use the routing system — add system.403 and system.404 routes. I think the changes introduced to the exception handlers are not necessary anymore (#10).
Comment #51
Wim Leers#50: Glad to hear that :)
One small correction: #50 still does change the default exception handler, but not anymore the default HTML exception handler.
Comment #52
Fabianx CreditAttribution: Fabianx commentedI tried to understand the issue, but I did not get it.
Why is this chunk needed at all?
Can't we just set directly the response we want?
Comment #53
znerol CreditAttribution: znerol commentedSee #18 and #19. We need to restore the wrapped response in the response subscriber of the master request. Otherwise it is impossible/brittle to detect an exception thrown by a form rendered in a subrequest (e.g. 403/404).
This can happen if, e.g. the login block is displayed on the access-denied page. Upon form submission a redirect is issued but the status code will be overridden by the exception handler (i.e. becomes 403 instead of 302). It is safer to restore the whole response after the exception was handled and that's what the hunk does.
Comment #54
Fabianx CreditAttribution: Fabianx commentedThanks for the explanation, do you feel the patch is ready?
Comment #55
znerol CreditAttribution: znerol commented#23 is not yet addressed:
Comment #56
znerol CreditAttribution: znerol commentedAdding tests (interdiff equals TEST-ONLY patch).
Comment #58
znerol CreditAttribution: znerol commentedAttempting to address #23.
Comment #59
Fabianx CreditAttribution: Fabianx commentedThis looks really great to me now! And I assume this is ready.
Has additional tests, fixes the issue, makes sense, feedback addressed => RTBC
Comment #60
dawehner+1 for this
418 my favourite HTTP status code!
Comment #61
alexpottSome minor nits found whilst reviewing. It would also be nice if the issue summary reflected the current solution and pointed to caveat documented in the patch. And I guess we should be opening a postponed 9.x critical to address this
explicitly
It should be
@param \Exception $e
Also missing @return. Perhaps something like
Should be
@param \Exception $previous
Can remove
from this class.
Comment #62
znerol CreditAttribution: znerol commentedThanks. Updated issue summary and patch.
Filed #2367555: Deprecate EnforcedResponseException support
Comment #63
Fabianx CreditAttribution: Fabianx commentedInterdiff looks great to me, back to RTBC
Comment #64
alexpott@znerol thanks for the fixes. This issue addresses a critical bug and is allowed per #2350615: [policy, no patch] What changes can be accepted during the Drupal 8 beta phase?. Committed dda70f7 and pushed to 8.0.x. Thanks!