renderPlain on a node render array causes RuntimeException if the node renders a view programmatically

Created on 24 March 2023, over 1 year ago

Problem

When using renderPlain() to send the HTML of a page over a 3rd party API, there is a Runtime Exception if the page renders any view programmatically (e.g. through a node preprocess). The renderPlain() was called from a shutdown function

Failed to start the session because headers have already been sent by "/app/vendor/symfony/http-foundation/Response.php" at line 384.

The site is currently on 9.5.5. I have tested with 9.5.6-dev and it has not been fixed. I have done some debugging and found that the bug was introduced in 9.5.4.

Steps to reproduce

  • Use Drupal core 9.5.4+
  • Create a node that has a programmatically generated view (i.e. $view = Views::getView()... $view->preview())
  • In a shut down function, call renderPlain() on a render array containing the above node

Proposed resolution

I have not got a solution. If I can find a fix I will create a patch and add it here.
If anyone thinks this is working as expected, then please make suggestions on how I can correct this.

πŸ› Bug report
Status

Active

Version

9.5

Component
ViewsΒ  β†’

Last updated about 2 hours ago

Created by

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @scambler
  • I have found the line which is causing the error and have reverted it to what it was on 9.5.3.

  • This next patch is more of a suggestion and not one I will be using on a production site (unless others approve it) as I am not confident of the consequences elsewhere on the site.

  • Thank you for reporting a bug.

    Can we get some sample reproduction code as a way to clarify the steps to reproduce please?

    Can you execute a git bisect, or a commit analysis to identify the core commit that changed the behavior please?

  • ^ I fixed a funny auto type in my comment.

  • *Correcting file path for patch 2

  • Status changed to Postponed: needs info over 1 year ago
  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    For #4

  • @cilefen I will create some sample code to help reproduce when I next get some free time

  • πŸ‡³πŸ‡±Netherlands Lendude Amsterdam

    Looking at the proposed change in #9 the culprit would have been πŸ› Feature "Remember the last selection" for views exposed filters doesn't work anymore Fixed , and can't recall anything else messing with sessions, so that sounds likely. That did add a check:

          if (empty($this->exposed_input) && $this->request->hasSession()) {
            $session = \Drupal::request()->getSession();
    

    But maybe that is not sufficient is some cases?

    • Create a node that has a programmatically generated view (i.e. $view = Views::getView()... $view->preview())
    • In a shut down function, call renderPlain() on a render array containing the above node

    Sounds pretty edge-case to me, so if that is indeed the only way to run into this, I'm not surprised we missed it.

  • The code basically goes like this:

    function my_module_node_update($node) {
      // Do some checks...
      // Do some stuff...
      
      drupal_register_shutdown_function('my_module_send_request', $node);
    }
    
    function my_module_send_request($node) {
      // Manually build up the page into a render array.
      $builder = Drupal::entityTypeManager()->getViewBuilder('node');
      $node_render_array = $builder->view($node, 'full');
      $build = [
        '#type' => 'html',
          'page' => [
            '#type' => 'page',
            '#title' => $node->label(),
            '#head_title' => [$node->label()],
            'content' => $node_render_array,
         ]
      ];
      
      Drupal::service('renderer')->renderPlain($build);
    
      return $build;
    }
    
    function my_module_preprocess_node(&$variables) {
      $view_id = 'watchdog';
      $display = 'page';
      if ($view = Views::getView($view_id)) {
        $view->setDisplay($display);
        $view->executeDisplay($display);
        $preview = $view->preview();
        $variables['content']['glossary'] = $preview;
        $variables['display'] = $display;
      }
    }
    

    The problem occurs in the renderPlain() when inside the preprocess node and executing/previewing the view. I have tried to tweak this so that it can work on a vanilla site, but I haven't tested it myself yet.

    Also worth noting that we have another process that is calls my_module_send_request manually and it works fine. Which further points at the shutdown function being the 'problem'.

  • Status changed to Needs review over 1 year ago
  • last update over 1 year ago
    30,321 pass, 1 fail
  • last update over 1 year ago
    30,322 pass
  • Status changed to Needs work over 1 year ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Believe next steps would be to write a test case or extend an existing test to show this issue.

  • πŸ‡­πŸ‡·Croatia devad

    Just to confirm that patch #9 fixed a node render RuntimeExceptions reported by search_api module in my case.

    RTBC in my case.

    My config: D10.3.1. The patch #9 applied successfully.

    Previous error message was:

    Type	search_api
    
    Date	Tuesday, July 30, 2024 - 23:55
    
    User	Anonymous (not verified)
    
    Location	https://my.com/node/2706/edit
    
    Referrer	https://my.com/node/2706/edit
    
    Message	RuntimeException while trying to render item entity:node/2706:en with view mode search_index for search index Default content index: Failed to start the session because headers have already been sent by "/home/my/public_html/vendor/symfony/http-foundation/Response.php" at line 1315. in Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start() (line 132 of /home/my/public_html/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php).
    
    Severity	Error
    
    Operations	
    
    Backtrace	
    #0 /home/my/public_html/core/lib/Drupal/Core/Session/SessionManager.php(162): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start()
    #1 /home/my/public_html/core/lib/Drupal/Core/Session/SessionManager.php(127): Drupal\Core\Session\SessionManager->startNow()
    #2 /home/my/public_html/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php(311): Drupal\Core\Session\SessionManager->start()
    #3 /home/my/public_html/vendor/symfony/http-foundation/Session/Session.php(222): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->getBag()
    #4 /home/my/public_html/vendor/symfony/http-foundation/Session/Session.php(242): Symfony\Component\HttpFoundation\Session\Session->getBag()
    #5 /home/my/public_html/vendor/symfony/http-foundation/Session/Session.php(69): Symfony\Component\HttpFoundation\Session\Session->getAttributeBag()
    #6 /home/my/public_html/core/modules/views/src/ViewExecutable.php(762): Symfony\Component\HttpFoundation\Session\Session->get()
    #7 /home/my/public_html/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php(2242): Drupal\views\ViewExecutable->getExposedInput()
    #8 [internal function]: Drupal\views\Plugin\views\display\DisplayPluginBase->elementPreRender()
    #9 /home/my/public_html/core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php(113): call_user_func_array()
    #10 /home/myi/public_html/core/lib/Drupal/Core/Render/Renderer.php(870): Drupal\Core\Render\Renderer->doTrustedCallback()
    #11 /home/my/public_html/core/lib/Drupal/Core/Render/Renderer.php(432): Drupal\Core\Render\Renderer->doCallback()
    #12 /home/my/public_html/core/lib/Drupal/Core/Render/Renderer.php(504): Drupal\Core\Render\Renderer->doRender()
    #13 /home/my/public_html/core/lib/Drupal/Core/Render/Renderer.php(504): Drupal\Core\Render\Renderer->doRender()
    #14 /home/my/public_html/core/lib/Drupal/Core/Render/Renderer.php(248): Drupal\Core\Render\Renderer->doRender()
    #15 /home/my/public_html/core/lib/Drupal/Core/Template/TwigExtension.php(475): Drupal\Core\Render\Renderer->render()
    #16 /home/my/public_html/sites/default/files/php/twig/66a95f91ba545_node.html.twig_NDqtWxoLpyRFa6oXHJ6UnJr_a/C7a4KCDRNPTvx_Z-GXBGdoFPJloemDJndA3-1dBnQp4.php(114): Drupal\Core\Template\TwigExtension->escapeFilter()
    #17 /home/krkswami/public_html/vendor/twig/twig/src/Template.php(360): __TwigTemplate_7a9ce1f8bd9ce10a30ff475b2491dced->doDisplay()
    #18 /home/my/public_html/vendor/twig/twig/src/Template.php(335): Twig\Template->yield()
    #19 /home/my/public_html/vendor/twig/twig/src/TemplateWrapper.php(38): Twig\Template->render()
    #20 /home/my/public_html/core/themes/engines/twig/twig.engine(33): Twig\TemplateWrapper->render()
    #21 /home/my/public_html/core/lib/Drupal/Core/Theme/ThemeManager.php(348): twig_render_template()
    #22 /home/my/public_html/core/lib/Drupal/Core/Render/Renderer.php(491): Drupal\Core\Theme\ThemeManager->render()
    #23 /home/my/public_html/core/lib/Drupal/Core/Render/Renderer.php(248): Drupal\Core\Render\Renderer->doRender()
    #24 /home/my/public_html/core/lib/Drupal/Core/Render/Renderer.php(165): Drupal\Core\Render\Renderer->render()
    #25 /home/my/public_html/core/lib/Drupal/Core/Render/Renderer.php(638): Drupal\Core\Render\Renderer->Drupal\Core\Render\{closure}()
    #26 /home/myi/public_html/core/lib/Drupal/Core/Render/Renderer.php(166): Drupal\Core\Render\Renderer->executeInRenderContext()
    #27 /home/my/public_html/modules/contrib/search_api/src/Plugin/search_api/processor/RenderedItem.php(222): Drupal\Core\Render\Renderer->renderInIsolation()
    #28 /home/my/public_html/core/lib/Drupal/Component/Utility/DeprecationHelper.php(40): Drupal\search_api\Plugin\search_api\processor\RenderedItem->Drupal\search_api\Plugin\search_api\processor\{closure}()
    #29 /home/my/public_html/modules/contrib/search_api/src/Plugin/search_api/processor/RenderedItem.php(223): Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall()
    #30 /home/my/public_html/modules/contrib/search_api/src/Item/Item.php(281): Drupal\search_api\Plugin\search_api\processor\RenderedItem->addFieldValues()
    #31 /home/my/public_html/modules/contrib/search_api/src/Entity/Index.php(987): Drupal\search_api\Item\Item->getFields()
    #32 /home/my/public_html/modules/contrib/search_api/src/Utility/PostRequestIndexing.php(92): Drupal\search_api\Entity\Index->indexSpecificItems()
    #33 /home/my/public_html/core/lib/Drupal/Core/DrupalKernel.php(723): Drupal\search_api\Utility\PostRequestIndexing->destruct()
    #34 /home/my/public_html/index.php(22): Drupal\Core\DrupalKernel->terminate()
    #35 {main}
  • πŸ‡ΊπŸ‡ΈUnited States bkosborne New Jersey, USA

    I've run into a similar problem, but my node doesn't contain a view, but instead contains a form. FormBuilder::buildForm has this chunk of code

        if ($request->getSession()->has('batch_form_state')) {
          // We've been redirected here after a batch processing. The form has
          // already been processed, but needs to be rebuilt. See _batch_finished().
          $session = $request->getSession();
          $form_state = $session->get('batch_form_state');
          $session->remove('batch_form_state');
          return $this->rebuildForm($form_id, $form_state);
        }
    

    So when it tries to build a form, it checks if the session has batch_form_state data set. If the session isn't yet started, it tries to start it, but NativeSessionStorage refuses to do so because the HTTP response headers have already been set (by the page's initial response).

    That code should probably check that the session is actually started before checking if it has data, but I'm not really sure.

Production build 0.71.5 2024