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.

Issue fork drupal-3350137

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

scambler created an issue. See original summary.

scambler’s picture

I have found the line which is causing the error and have reverted it to what it was on 9.5.3.

scambler’s picture

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.

cilefen’s picture

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?

cilefen’s picture

^ I fixed a funny auto type in my comment.

scambler’s picture

Component: render system » views.module
scambler’s picture

*Correcting file path for patch 2

larowlan’s picture

Status: Active » Postponed (maintainer needs more info)

For #4

scambler’s picture

StatusFileSize
new829 bytes

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

lendude’s picture

Looking at the proposed change in #9 the culprit would have been #3260652: Feature "Remember the last selection" for views exposed filters doesn't work anymore, 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.

scambler’s picture

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'.

scambler’s picture

Status: Postponed (maintainer needs more info) » Needs review

The last submitted patch, 7: core-render-plain-runtime-exception-3350137-7.patch, failed testing. View results

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative, +Needs tests

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

Version: 9.5.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

devad’s picture

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

RTBC for me.

My config: D10.3.1. The patch #9 applies 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}
bkosborne’s picture

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.

dejan0’s picture

Yes, I’m experiencing the same issue with FormBuilder::buildForm. The $request->getSession()->has('batch_form_state') check always fails when headers have already been sent.

Symfony’s NativeSessionStorage (symfony/http-foundation) explicitly throws an exception in this case: https://github.com/symfony/http-foundation/blob/7.3/Session/Storage/Nati...

It might make sense to add this check in SessionManager::startNow() to handle this case better...

ankondrat4’s picture

Patch #18 https://www.drupal.org/project/drupal/issues/3350137#comment-16014932 works for me on Drupal 10.5.0
I have created issue fork with these changes.
Please review.

ankondrat4’s picture

Status: Needs work » Needs review
smustgrave’s picture

Status: Needs review » Needs work

Thanks! See it was previously tagged for tests so moving to NW for that.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

tvalimaa’s picture

The fix (!headers_sent() && parent::start()) is already present in SessionManager.php at line 138 — it's been merged into Drupal core 11.4.5.