Score:0

$node->save() fails when run in post_update

us flag

I have a simple hook_post_update function which fails with the following error:

[notice] Update started: ssc_custom_post_update_9001

[error] LogicException: Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead. in Drupal\Core\Render\Renderer->doRender() (line 244 of E:\www\myssc\html\core\lib\Drupal\Core\Render\Renderer.php). [error] Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead. [error] Update failed: ssc_custom_post_update_9001

In ProcessBase.php line 171:

Unable to decode output into JSON: Syntax error

[ERROR] [node] [2022-03-12T04:58:35] LogicException: Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_re nder instead. in Drupal\Core\Render\Renderer->doRender() (line 244 of E:\www\myssc\html\core\lib\Drupal\C ore\Render\Renderer.php). | uid: 0 | request-uri: http://default/ | refer: | ip: 127.0.0.1 | link:
{ "0": { "ssc_custom": { "9001": { "#abort": { "success": false, "query": "Drupal\Core\Entity\EntityStorageException: Ren der context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use re nderPlain()/renderRoot() or #lazy_builder/#pre_render instead. in Drupal\Core\Ent
ity\Sql\SqlContentEntityStorage->save() (line 810 of E:\www\myssc\html\core\lib\Drupal\Core\Entity\Sql\SqlContentEntityStorage.php)." } } }, "#abort": [ "ssc_custom_post_update_9001" ] }, "drush_batch_process_finished": true }

I have stripped the code down to the bare minimum:

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\node\NodeInterface;

function ssc_custom_post_update_9001(&$sandbox) {
  /** @var \Drupal\node\NodeStorageInterface $node_storage */
  $node_storage = \Drupal::entityTypeManager()->getStorage('node');

  // How many entities to processed per batch.
  $limit = 5;

  $node_ids = $node_storage->getQuery()
    ->accessCheck(FALSE)
    ->condition('type', 'article')
    ->range(0, $limit)
    ->execute();
  // Load entities.
  $nodes = $node_storage->loadMultiple($node_ids);

  /** @var \Drupal\node\NodeInterface $node */
  foreach ($nodes as $node) {
    $node->setNewRevision();
    $node->save();
  }

  $sandbox['#finished'] = 1;

}

If I run the same code directly (not from drush updb) it runs fine. Running from the admin menu "Run updates"; it also fails (so it is not a drush issue).

Commenting out the $node->save() and the error does not occur.

sonfd avatar
in flag
Does this happen if you load and save a different content type?
leymannx avatar
ne flag
This is not how batch works. Content updates should happen in `hook_deploy_N` running after configuration import when executing `drush deploy`.
4uk4 avatar
cn flag
If an error occurs in a Drupal function used in many different places you have to check the call stack from where it is called. This might point to a module which hooks into the node save process.
liquidcms avatar
us flag
Hmm, oddly i am no longer getting notifications from Drupal Answers.. @sonfd, yes, any content type. leymannx, not sure what hook_deploy is, will take a look; but hook_post_update states it is specifically for tasks like this when you need to update site content and examples even show using batch and node save: https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Extension%21module.api.php/function/hook_post_update_NAME/9.0.x
liquidcms avatar
us flag
@4uk4, yes, that was my next plan.. debug in core to see where this fails and see if i can tell which module is causing this. I have removed all my custom code (which did have node save) and it wasnt the issue. Odd there isn't a way to run an update that is able to bypass this issue as it does seem silly to have fix a dozen (yes, likely not that many) contrib modules to do a simple content update. Alternatively i can give my clients a button to push after the site is upgraded; but certainly a regression from D7 that i can't have my deploy script do the update.
liquidcms avatar
us flag
@leymannz, there is no such thing as hook_deploy - possibly something added by a contrib module? Deploy perhaps?
cn flag
It’s a drush hook @liquidcms
Score:2
cn flag

You can reproduce the error with Drush:

# drush php

>>> $build = ['#markup' => 'TEST'];
=> [
     "#markup" => "TEST",
   ]

>>> $rendered = \Drupal::service('renderer')->render($build);
LogicException with message 'Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.'

>>> $rendered = \Drupal::service('renderer')->renderPlain($build);
=> Drupal\Core\Render\Markup {#4668
     markup: "TEST",
   }

So as the error message says modules which hook into the node save process should use renderPlain() instead. They shouldn't assume nodes are always saved within a render context of a themed Drupal page.

Code example for rendering a view in isolation:

$build = $view->buildRenderable();
$rendered = \Drupal::service('renderer')->renderPlain($build);
liquidcms avatar
us flag
Turns out it is my custom code causing the issue. I have code which runs on node save/presave which is trying to get values from a View and is using advancedRender() which i assume is causing this issue. Disappointing that my code works as it should when nodes are normally being saved but in post_update i am blocked from doing this. Easy enough to work around as these functions dont need to be run during this update - but odd I would need to do this hack.
4uk4 avatar
cn flag
If the render() function is not in your control, like here in a View, you can enclose the code in your own RenderContext and discard the bubbled up metadata. Drupal does this for example when rendering mails. See https://drupal.stackexchange.com/questions/245715/how-to-get-cache-metadata-from-nested-render-array-when-returning-response
liquidcms avatar
us flag
thanks for the tip.. good to know. Although a lot of work to simply copy a field to a new field as a 1 time ting. For now i simply set a static var in the update script and then use that to ignore my save/presave functions as they don't need to be run when this update is being run.
4uk4 avatar
cn flag
OK, but this doesn't solve the problem with leaking metadata into an unknown render context. Running render() or code containing render() should only bubble up metadata if you can make sure the render context belongs to the rendered content. Most times this is the page render context when theming page elements.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.