diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 02620cbf11..f6664dfb09 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -353,6 +353,23 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Build the element if it is still empty. if (isset($elements['#lazy_builder'])) { $new_elements = $this->doCallback('#lazy_builder', $elements['#lazy_builder'][0], $elements['#lazy_builder'][1]); + // Throw an exception if #lazy_builder callback does not return an array; + // provide helpful details for troubleshooting. + if (!is_array($new_elements)) { + $callable = $elements['#lazy_builder'][0]; + $callable_name = '[unknown]'; + if ($callable instanceof \Closure) { + $callable_name = '[closure]'; + } + elseif (is_array($callable)) { + $callable_name = implode('::', $callable); + } + elseif (is_string($callable)) { + $callable_name = $callable; + } + $wrong_type = gettype($new_elements); + throw new \LogicException("#lazy_builder callbacks must return a valid renderable array, got $wrong_type from " . Xss::filter($callable_name)); + } // Retain the original cacheability metadata, plus cache keys. CacheableMetadata::createFromRenderArray($elements) ->merge(CacheableMetadata::createFromRenderArray($new_elements)) diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php index 7478840c5d..4c6f40c6da 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php @@ -962,6 +962,53 @@ public function testCreatePlaceholderPropertyWithoutLazyBuilder() { $this->renderer->renderRoot($element); } + /** + * Tests lazy builders (string callable) that do not return a renderable. + * + * @covers ::render + * @covers ::doRender + */ + public function testNonArrayReturnFromLazyBuilderStringCallable() { + $element = []; + $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\PlaceholdersTest::callbackNonArrayReturn', []]; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('#lazy_builder callbacks must return a valid renderable array, got boolean from Drupal\Tests\Core\Render\PlaceholdersTest::callbackNonArrayReturn'); + $this->renderer->renderRoot($element); + } + + /** + * Tests lazy builders (array callable) that do not return a renderable. + * + * @covers ::render + * @covers ::doRender + */ + public function testNonArrayReturnFromLazyBuilderArrayCallable() { + $element = []; + $element['#lazy_builder'] = [['Drupal\Tests\Core\Render\PlaceholdersTest', 'callbackNonArrayReturn'], []]; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('#lazy_builder callbacks must return a valid renderable array, got boolean from Drupal\Tests\Core\Render\PlaceholdersTest::callbackNonArrayReturn'); + $this->renderer->renderRoot($element); + } + + /** + * Tests lazy builders (closure) that do not return a renderable. + * + * @covers ::render + * @covers ::doRender + */ + public function testNonArrayReturnFromLazyBuilderClosure() { + $element = []; + $element['#lazy_builder'] = [function () { + return NULL; + }, []]; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('#lazy_builder callbacks must return a valid renderable array, got NULL from [closure]'); + $this->renderer->renderRoot($element); + } + /** * Create an element with a child and subchild. Each element has the same * #lazy_builder callback, but with different contexts. They don't modify diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php index 6512194631..49c6b361b5 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php @@ -317,11 +317,21 @@ public static function callbackTagCurrentTemperature($animal) { return $build; } + /** + * Returns invalid renderable to #lazy_builder callback. + * + * @return bool + * TRUE. + */ + public static function callbackNonArrayReturn() { + return TRUE; + } + /** * {@inheritdoc} */ public static function trustedCallbacks() { - return ['callbackTagCurrentTemperature', 'callbackPerUser', 'callback']; + return ['callbackTagCurrentTemperature', 'callbackPerUser', 'callback', 'callbackNonArrayReturn']; } }