Score:2

Debugging "UNCACHEABLE" header

in flag

We have a site where the homepage is not being cached and contains the headers:

x-cache: MISS, MISS
x-cache-hits: 0, 0
x-content-type-options: nosniff
x-drupal-dynamic-cache: UNCACHEABLE

I narrowed this down to the content regions contents, and disabled the "Main page content" for the front page. This then gave me a cache HIT, and no longer responded as UNCACHEABLE. From there, I narrowed it down to a field formatter being used on a paragraph. We have a custom one that extends the normal entity render formatter.

If I swap it back to the original "Render entity" formatter, everything is fine. So then, it must be something we are doing in this custom formatter causing the issue.

I can see when I follow with xdebug that shouldCacheResponse of DynamicPageCacheSubscriber returns FALSE, because something is setting max-age to 0 (not by code). It looks like calls to addCacheableDependency may be triggering this behavior in the formatter:

Essentially, the formatter adds cache data to the render so if any of its referenced items are updated, the cache should be invalidated for that host paragraph so it re-renders:

     $view_builder = \Drupal::entityTypeManager()->getViewBuilder($entity->getEntityTypeId());
      $elements[$delta] = $view_builder->view($entity, $view_mode, $entity->language()->getId());

      try {
        $parent = $items->getParent();
        $parent_entity = $parent->getValue();
        $elements[$delta]['#cache']['keys'][] = $parent_entity->id();
        $elements[$delta]['#cache']['keys'][] = $parent_entity->bundle();
        $elements[$delta]['#cache']['keys'][] = $parent_entity->getEntityTypeId();
        $elements[$delta]['#cache']['keys'][] = 'delta_' . $delta;
        $elements[$delta]['#cache']['keys'][] = 'context_aware';

        $this->renderer->addCacheableDependency($elements[$delta], $parent);

        if ($entity->hasField('field_author')) {
          $child = $entity->field_author->entity;

          if (isset($child)) {
            $this->renderer->addCacheableDependency($elements[$delta], $child);
          }
        }

        // similar statements with addCacheableDependency

If I comment out this initial line:

$this->renderer->addCacheableDependency($elements[$delta], $parent);

Then I get a cacheable response. This looks to be because the $parent item (even though it is a node or paragraph or media entity) triggers this:

  /**
   * Creates a CacheableMetadata object from a depended object.
   *
   * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $object
   *   The object whose cacheability metadata to retrieve. If it implements
   *   CacheableDependencyInterface, its cacheability metadata will be used,
   *   otherwise, the passed in object must be assumed to be uncacheable, so
   *   max-age 0 is set.
   *
   * @return static
   */
  public static function createFromObject($object) {
    if ($object instanceof CacheableDependencyInterface) {
      $meta = new static();
      $meta->cacheContexts = $object->getCacheContexts();
      $meta->cacheTags = $object->getCacheTags();
      $meta->cacheMaxAge = $object->getCacheMaxAge();
      return $meta;
    }

    // Objects that don't implement CacheableDependencyInterface must be assumed
    // to be uncacheable, so set max-age 0.
    $meta = new static();
    $meta->cacheMaxAge = 0;
    return $meta;
  }

Setting cacheMaxAge to 0 because it is not an instance of CacheableDependencyInterface.

If I am already setting the cache keys, is this line even needed:

$this->renderer->addCacheableDependency($elements[$delta], $parent);

If I remove that, will there be an adverse effect (like render displays not re-rendering when referenced items are saved)?

4uk4 avatar
cn flag
Setting cache keys is not enough, you need the cache tags as well. So don't remove this line, just check the object is not NULL.
sonfd avatar
in flag
Yeah cache keys don't really do anything - contexts, tags, and max-age are what you'd want to make sure to carry over.
Kevin avatar
in flag
$parent is not null, but is received in createFromObject as an instance of EntityAdapter (containing the entity node, or paragraph) which I can't trace as implementing CacheableDependencyInterface
4uk4 avatar
cn flag
OK, now I see the problem, the entity with the cache data is `$parent_entity`.
Kevin avatar
in flag
That is what I suspected, thanks for confirming. Changing that returns a cacheable response to the browser.
Score:4
in flag

This was a good debug deep dive. As mentioned by 4k4 the problem is the first addCacheableDependency line.

Instead of passing the host entity itself, I was mistakenly passing the object returned from getParent which is a TypedData instance that does not implement CacheableDependencyInterface - thus causing the max-age to be set to 0 and the UNCACHEABLE header result.

Passing the entity (returned from getValue()) resolved the issue:

    $parent = $items->getParent();
    $parent_entity = $parent->getValue();
    ...
    $this->renderer->addCacheableDependency($elements[$delta], $parent_entity);
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.