Problem/Motivation

I can't seem to edit any fields for existing views. Steps to reproduce:

  1. Do a fresh install of HEAD (I am on 2219b31)
  2. Goto admin/structure/views/view/user_admin_people
  3. Click on anything in Fields to edit it.
  4. See the Views spinner briefly, then nothing.

The exception is the following ...

The website encountered an unexpected error. Please try again later.
LogicException: The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: Drupal\Core\Ajax\AjaxResponse. in Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext() (line 145 of core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php).
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
call_user_func_array(Object, Array)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1)
Stack\StackedHttpKernel->handle(Object, 1, 1)
Drupal\Core\DrupalKernel->handle(Object)

I started this as a Major, but I really think this is a Critical, as it makes editing anything with Views unusable.

Proposed resolution

Update FieldPluginBase::buildOptionsForm() to use a proper render array, rather than hardcoded and rendered output.

Build the Views AJAX forms in a render context in order to ensure that cacheable metadata is bubbled up.

Update Views testing bases class to double check that corresponding AJAX paths for NOJS requests work properly.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because Views functionality is currently broken.
Issue priority Critical because Views is essentially useless to the majority of site owners without being able to edit field output.
Unfrozen changes Unfrozen because it addresses a critical bug.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mpdonadio’s picture

Issue summary: View changes
dawehner’s picture

Priority: Major » Critical
Issue tags: +Needs tests, +D8 Accelerate London

This is certainly a critical, given that the views UI is pretty much entirely broken!

dawehner’s picture

Issue summary: View changes
dawehner’s picture

Status: Active » Needs review
FileSize
659 bytes

This is the first problem I found.

plach’s picture

Status: Needs review » Needs work

Manually tested and the patch fixes the issue. Settings to NW for missing tests.

plach’s picture

Assigned: Unassigned » plach

Working on test coverage

Wim Leers’s picture

plach’s picture

Assigned: plach » Unassigned
Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
3.03 KB

Test coverage

plach’s picture

Without useless cruft

dawehner’s picture

FileSize
3.26 KB
4.58 KB

Let's do it right this time.

martin107’s picture

Issue summary: View changes

s/see/seem

xjm’s picture

Wow... that's an impressive bug. Edit: And yeah, this is critical.

xjm’s picture

Issue tags: +VDC

The last submitted patch, 9: views-ui_field_broken-2527606-9.test.patch, failed testing.

xjm’s picture

Also, +1 for the render array use replacing the SafeMarkup::set() call. It's actually reassuring that that is the fix and means our larger critical objectives are actually on the right track. :)

catch’s picture

+++ b/core/modules/views_ui/src/Tests/FieldUITest.php
@@ -57,6 +57,11 @@ public function testFieldUI() {
+    $this->assertResponse(200);

Anything worth asserting other than the 200? Content type header maybe?

Otherwise looks great.

xjm’s picture

There was a crosspost that made the result reporting goofy for the test-only patch, but it does fail locally. I also managed to dig the result out of the testbot logs:
https://qa.drupal.org/pifr/test/1088358

Edit: I crossposted with catch; +1 for adding an additional assertion.

Wim Leers’s picture

Nit: unwanted space in

[ '#markup']
mpdonadio’s picture

Issue summary: View changes
mpdonadio’s picture

Just repeating what @catch and @xjm said, but with a slightly different idea...

+++ b/core/modules/views_ui/src/Tests/FieldUITest.php
@@ -57,6 +57,11 @@ public function testFieldUI() {
+
+    // Ensure the AJAX UI is responding.
+    $ajax_url = 'admin/structure/views/ajax/handler/test_view/default/field/age';
+    $this->drupalGet($ajax_url);
+    $this->assertResponse(200);
   }

Given the severity of this bug, and the fact that we didn't have a test that caught it, is this addition sufficient? How about testing the non-JS path, checking for a 200, and then making sure the options form got rendered?

dawehner’s picture

FileSize
4.12 KB
2.33 KB

Here is an idea, "hack" drupalGet() with nojs.

larowlan’s picture

+++ b/core/modules/views_ui/src/Tests/UITestBase.php
@@ -74,4 +74,21 @@ public function randomView(array $view = array()) {
+      $this->drupalGet($url, $options, $headers);

How much overhead does this add to every views UI test? Not saying I don't love the idea, but should it be opt-in rather than opt-out?

Also, should we document the extra 'no_ajax_check' option on this method instead of using {@inheritdoc}

Also what happens if there is no 'ajax' in the $path, - we'd end up with a duplicate request to the same path right? because str_replace($url, ...) would be the same as the previous $url?

Also I think I've used my quota of alsos.

dawehner’s picture

  1. +++ b/core/modules/views_ui/src/Tests/UITestBase.php
    @@ -74,4 +74,21 @@ public function randomView(array $view = array()) {
    +    // Ensure that each nojs page is accessible via ajax as well.
    +    if (empty($options['no_ajax_check']) && strpos($url, 'nojs') !== FALSE) {
    

    Well, here it is checking whether nojs is in there.

  2. +++ b/core/modules/views_ui/src/Tests/UITestBase.php
    @@ -74,4 +74,21 @@ public function randomView(array $view = array()) {
    +      $this->drupalGet($url, $options, $headers);
    +      $this->assertResponse(200);
    

    I think the additional request is worth the additional test coverage

larowlan’s picture

Whoops, yeah I misunderstood, we're going the other way (nojs » ajax), ignore me - so I think we should just document the no_ajax_check option on the overridden method.

dawehner’s picture

FileSize
4.08 KB
716 bytes

Given that the patch is green, I think its fine for now to just skip the option.

mpdonadio’s picture

I'm confused. In HEAD, why does admin/structure/views/nojs/handler/user_admin_people/page_1/field/created work, but admin/structure/views/ajax/handler/user_admin_people/page_1/field/created throws a 500?

Is this because of #2450993: Rendered Cache Metadata created during the main controller request gets lost, that in the ajax callback FieldPluginBase::buildOptionsForm() is being called outside of a rendering context, while the from nojs callback it is in one? If that is the case, it seems like we should do a full test run on a side issue with the hunk in UITestBase moved to WebTestBase (well, that may get tricky) to see if there are any other hidden gotchas.

plach’s picture

Status: Needs review » Reviewed & tested by the community

Looking at logs the test suite execution time does not seem to be heavily affected by the additional test coverage.

Aside form that, I agree with @mpdonadio that the fact that the error happens only in the AJAX context could use some additional investigation. However I think the suspect that something more might be wrong is not enough to hold off a critical issue, so I think a non-critical follow-up would be better for that.

My tiny test coverage patch was completely superseded by the new generic test coverage, so I think it's fine for me to RTBC this.

catch’s picture

Status: Reviewed & tested by the community » Needs work

I opened #2528124: Additional simpletest and/or manual testing of Views UI for follow-up testing.

But I still think we should check content type header or something to ensure this is a valid response. I'm sure Drupal 8 was fataling with a 200 response at one point.

Fabianx’s picture

+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -875,10 +875,12 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+      $output = [];
+      $output[]['#markup'] = '<p>' . $this->t('You must add some additional fields to this display before using this field. These fields may be marked as <em>Exclude from display</em> if you prefer. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.') . '</p>';
...
-        $output = '<p>' . $this->t("The following replacement tokens are available for this field. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.") . '</p>';
+        $output[]['#markup'] = '<p>' . $this->t("The following replacement tokens are available for this field. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.") . '</p>';

That is a total nit, but:

$output[]['#markup'] =

looks totally strange to my eyes ...

It creates a new element, then makes it empty, then adds #markup to it, then adds data to that.

What about:

$output[] = ['#markup' => ... ]

instead?

---

Overall except for catch' remark, RTBC + 1

Wim Leers’s picture

+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -890,7 +892,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
-            $output .= $this->getRenderer()->render($item_list);

Rather than fixing this one case, of which there will likely be more in other Views plugins/handlers, I think it'd be better to update this code from:

    $form = \Drupal::formBuilder()->buildForm($form_class, $form_state);
    $output = $renderer->renderRoot($form);

to:

    // The plugins and handlers that buildForm() calls may still be doing early
    // rendering. Rather than fixing them all, execute them in a render context
    // so that EarlyRenderingControllerWrapperSubscriber does not complain.
    $render_context = new RenderContext();
    $form = $renderer->executeInRenderContext($render_context, function() {
      return \Drupal::formBuilder()->buildForm($form_class, $form_state);
    });
    BubbleableMetadata::createFromRenderArray($form)
      ->merge($render_context->pop())
      ->applyTo($form);
    $output = $renderer->renderRoot($form);
dawehner’s picture

Status: Needs work » Needs review
FileSize
5.27 KB
2.61 KB

Yeah, there we go.

xjm’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
    @@ -205,8 +206,13 @@ protected function ajaxFormWrapper($form_class, FormStateInterface &$form_state)
    +    $render_context = new RenderContext();
    +    $callable = function () use ($form_class, &$form_state) {
    +      return \Drupal::formBuilder()->buildForm($form_class, $form_state);
    +    };
    +    $form = $renderer->executeInRenderContext($render_context, $callable);
    

    Inline comment would be super here.

  2. +++ b/core/modules/views_ui/src/Tests/FieldUITest.php
    @@ -57,6 +57,11 @@ public function testFieldUI() {
    +    // Ensure the AJAX UI is responding.
    +    $ajax_url = 'admin/structure/views/ajax/handler/test_view/default/field/age';
    +    $this->drupalGet($ajax_url);
    +    $this->assertResponse(200);
    

    NW for the additional test assertion (see @catch's comment above).

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.91 KB
2.52 KB

Sure, let's do that. I removed the test coverage from FieldUITest because the generic drupalGet() one covers that already, but much better.

Fabianx’s picture

Status: Needs review » Needs work
Issue tags: +D8 Accelerate
+++ b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
@@ -205,8 +206,13 @@ protected function ajaxFormWrapper($form_class, FormStateInterface &$form_state)
+    $form = $renderer->executeInRenderContext($render_context, $callable);
     $output = $renderer->renderRoot($form);

The render_context needs to be applied still to $form.

=> CNW

dawehner’s picture

Status: Needs work » Needs review
FileSize
5.08 KB
792 bytes

... this was a old comment

Wim Leers’s picture

Status: Needs review » Needs work
  1. index f17df18..3d1775d 100644
    --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
    

    The fact that we now wrap the form building in a render context means that all changes here are now technically no longer necessary, but they're still an improvement of course.

    I'll leave it to a committer to decide whether to do the right, yet strictly speaking now unnecessary thing.

  2. +++ b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
    @@ -205,8 +207,21 @@ protected function ajaxFormWrapper($form_class, FormStateInterface &$form_state)
    +    // Render the form in a render context in order to ensure that cacheable
    +    // metadata is bubbled up.
    

    This does NOT render the form, it builds the form. This should probably mention that this specifically wants to allow Views handlers/plugins that provide forms to still do early rendering. If they didn't do early rendering, this would not be necessary.

  3. +++ b/core/modules/views_ui/src/Tests/UITestBase.php
    @@ -74,4 +74,23 @@ public function randomView(array $view = array()) {
    +    // Ensure that each nojs page is accessible via ajax as well.
    

    Oh, nice, this is very comprehensive for sure :)

dawehner’s picture

Status: Needs work » Needs review
FileSize
5.08 KB
665 bytes
5.08 KB
712 bytes

I'll leave it to a committer to decide whether to do the right, yet strictly speaking now unnecessary thing.

I think fixing the UI with that is not a bad idea.

plach’s picture

Status: Needs review » Reviewed & tested by the community

Reviews above are addressed back to RTBC.

Patch to commit is #37 (https://www.drupal.org/files/issues/2527606-37.patch).

mpdonadio’s picture

Issue summary: View changes

Updated IS to match approach in #37.

Fabianx’s picture

RTBC + 1


Unrelated:

+++ b/core/modules/views_ui/src/Form/Ajax/ViewsFormBase.php
@@ -205,8 +207,21 @@ protected function ajaxFormWrapper($form_class, FormStateInterface &$form_state)
+      BubbleableMetadata::createFromRenderArray($form)
+        ->merge($render_context->pop())
+        ->applyTo($form);

I will open a follow-up to add a $render_context->applyTo() method directly.

Edit: Opened #2528322: Add RenderContext::applyTo method

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Nice one less SafeMarkup::set() too! Committed c78d806 and pushed to 8.0.x. Thanks!

  • alexpott committed c78d806 on 8.0.x
    Issue #2527606 by dawehner, plach, mpdonadio, xjm, Wim Leers, Fabianx:...
Wim Leers’s picture

RTBC +1

Too late :) Congrats!

catch’s picture

FileSize
1022 bytes

I applied just the test changes, ran a couple of Views UI tests, nothing failed.

Here's a test-only patch. I'm wondering if we got the exception because we were in an AJAX response, and then we updated the tests to use non-AJAX, so now we're not testing it at all again.

So if this issue goes to CNW, it's RTBC. And if it stays at RTBC, it's CNW ;)

catch’s picture

I don't think I applied the patch before running the tests locally, which obviously would not fail 'cos that's just HEAD. So ignore #44 but it's always nice to have test-only patches on issues for confidence.

alexpott’s picture

I can confirm Drupal\views_ui\Tests\FieldUITest fails with #44 applied.

  • alexpott committed 7b9c39c on 8.0.x
    Issue #2527606 by dawehner, plach, catch, mpdonadio, Wim Leers, xjm,...
  • alexpott committed 81087eb on 8.0.x
    Revert "Issue #2527606 by dawehner, plach, mpdonadio, xjm, Wim Leers,...
alexpott’s picture

The revert and re-commit added @catch to the list of people credited.

Wim Leers’s picture

Interesting how the re-commit showed up before the revert. That really confused me :)

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.