You are here

protected function ExecuteInRenderContextTrait::executeInRenderContext in SAML Authentication 4.x

Same name and namespace in other branches
  1. 8.3 src/Controller/ExecuteInRenderContextTrait.php \Drupal\samlauth\Controller\ExecuteInRenderContextTrait::executeInRenderContext()

Executes code within a render context; logs if leaked metadata is found.

Generally, the reason for using this method is to guard against Drupal throwing "leaked metadata" exceptions, by requests which are required to return a response that implements CacheableResponseInterface. (In practice these are e.g. TrustedRedirectResponse and various ResourceResponses.)

Metadata is (nowadays) generally 'leaked' by completely unrelated code, usually an Url::toString() call somewhere - so these fatal errors could start happening by simply upgrading a contrib module. (The 'principled' stance is that the cause of those errors should be traced and fixed instead of ignored. However, that principled stance does not protect an application from becoming unstable.)

WARNING: since this method discards the leaked cacheability metadata, it is only suitable for responses whose contents

  • will never be cached (despite implementing CacheableResponseInterface - e.g. because "no_cache: TRUE" is set in the routing.yml)
  • or are very predictable and well known, and there is a guarantee that those contents are not influenced by whatever bug that led to metadata being leaked.

Reason being: the leaked metadata is quite unlikely to corrupt the contents of the current response, but it can corrupt future responses' contents, by caching the response in the wrong circumstances.

---

History / reason for why this is a thing: 1. Url objects and Drupal rendering quick primer:

  • It is quite important for Drupal's render cache, to receive correct metadata along with a response, indicating in exactly when / for which cases the response is cacheable. Wrong caching of rendered objects can e.g. introduce security issues. This is especially the case with URLs that could contain tokens.
  • Most execution paths in a Drupal controller return a 'render array' (not a Response object), and execute within a 'render context'. Quote RendererInterface::executeInRenderContext phpDoc: "Within a render context, all bubbleable metadata is bubbled and hence tracked. Outside of a render context, it would be lost."
  • Url::toString() automatically inserts related cacheability metadata into the current render context, to prevent the mentioned issues, since #2351015. Url::toString(TRUE) instead returns the metadata back to the caller along with the URL string.

2. What's the problem:

  • (Since #2450993) Drupal executes all controller routes within a render context, and afterwards checks if any metadata is present in the context. If the route/method returned a render array, the metadata is used; if it is a cacheable Response object, Drupal throws an exception in that case, saying "leaked metadata" was detected which indicates "early rendering". [*1]
  • Nowadays, the vast(?) majority of data inadvertently 'leaked into' a render context comes from Url::toString() calls, because an average developer doesn't know about the intricacies of cacheability / what these calls do in the background. [*2]
  • The combination of these points means that our code is going to end in a "leaked metadata" exception, each time some 'external' code happens to call Url::toString() that is not executed within a render context that's explicitly used to handle the call's metadata. This 'external' code can be event subscribers, hooks... even Url::fromRoute() and toString(TRUE) calls themselves can execute other code that then calls Url::toString() which causes the exception. [*3]

So: The only way to reliably prevent "leaked metadata" exceptions is to create a render context ourselves, and execute any of our code that could invoke 'external' code.

*1 If interested why this wording is used: see EarlyRenderingControllerWrapperSubscriber. Many discussions have been had about the effects of this, for instance: https://www.lullabot.com/articles/early-rendering-a-lesson-in-debugging-... / #2630808 / #2638686 / #2450993-133. The second issue has basically been stuck on the assertions in the latter message since January 2016, but... the majority of those assertions are incorrect, as the above summary makes clear. (I'm sure that was less clear in 2016 when more 'early rendering' was still happening.) *2 The issue surely is more prevalent because of Url::toString()'s lack of documentation. The casual developer doesn't know that constructing a simple URL requires them to consider cacheability, and why. The $collect_bubbleable_metadata argument to toString() is unintelligible for them (who knows what 'bubbleable metadata' is?), the code is nearly impossible for them to decipher, and the string "cache" occurs zero times in the Url class. *3 Examples of Url::fromRoute() and Url::toString(TRUE) indirectly executing another offending Url::toString() call: #3161036 / #3160515-35 (both present in Rules 8.x-3.0-alpha6)

Parameters

callable $callable: A callable whose return value will be returned by this method.

string $while: (Optional) description of when we're doing this, for error logging.

Return value

mixed The return value of the callable.

1 call to ExecuteInRenderContextTrait::executeInRenderContext()
ExecuteInRenderContextTrait::getTrustedRedirectResponse in src/Controller/ExecuteInRenderContextTrait.php
Executes code in a render context; generates a TrustedRedirectResponse.

File

src/Controller/ExecuteInRenderContextTrait.php, line 119

Class

ExecuteInRenderContextTrait
Helper code for executing a callable inside a render context.

Namespace

Drupal\samlauth\Controller

Code

protected function executeInRenderContext(callable $callable, $while = '') {
  $context = new RenderContext();
  $result = $this->renderer
    ->executeInRenderContext($context, $callable);
  if (!$context
    ->isEmpty() && isset($this->logger)) {

    // Some code 'leaked' metadata. We cannot do anything about this / likely
    // do not suffer negative consequences from this, but it should still be
    // fixed. Log a warning, in the hope that someone sees it and traces the
    // offending code. (For anyone reading this comment while investigating
    // the below warning log: an explanation is given above, and
    // https://www.lullabot.com/articles/early-rendering-a-lesson-in-debugging-drupal-8
    // documents a tedious debugging session looking for a similar bug.)
    $prefix = $while ? "While {$while}, code" : 'Code';

    // There should be few distinct permutations of $while, so (while still
    // not ideal) it's OK to include in the translatable string.
    $this->logger
      ->warning("{$prefix} leaked cacheability metadata. This indicates a bug somewhere (but it is hard to pinpoint where): if the same code is called in other scenarios too, it may cause fatal crashes, or bloat the render cache unnecessarily. Please investigate. Metadata: @data", [
      '@data' => $context
        ->serialize(),
    ]);
  }
  return $result;
}