Debugging a 'x-drupal-dynamic-cache: UNCACHEABLE' header in Drupal

Recently, I noticed that the x-drupal-dynamic-cacheheader on one of my client's websites was being returned as UNCACHEABLEfor all pages on the site, even for anonymous users. This didn't add up, because, when I launched the site, that wasn't the case, and I hadn't made any changes to the custom code that would make the entire site uncacheable by the dynamic page cache.

Dynamic Page Cache

When the Dynamic Page Cache module (part of core) decides what's cacheable, it references the auto_placeholder_conditionsvalue in the renderer.configservices parameter. The default value is provided by /core/core.services.yml:

    auto_placeholder_conditions:
      max-age: 0
      contexts: ['session', 'user']
      tags: []

If a render array has one of these values set in a #cachesub-array, then the entire render array be considered uncacheable.

(Site builders can override these values by copying /sites/default/default.services.yml to /sites/default/services.yml and customizing the values.)

Debugging

The next step was to enable and to examine some debugging headers, X-Drupal-Cache-Tags, X-Drupal-Cache-Contexts, and X-Drupal-Cache-Max-Age. This can be accomplished by copying /sites/default/default.services.yml to /sites/default/services.yml and setting http.response.debug_cacheability_headersto true. Now when I loaded a page as an anonymous user, I could see the factors that were determining cacheability:

x-drupal-cache-contexts: cookies:big_pipe_nojs languages:language_interface route session.exists theme url.path.is_front url.path.parent url.query_args url.site user

x-drupal-cache-max-age: -1 (Permanent)

x-drupal-cache-tags: block_view config:block.block.olivero_account_menu config:block.block.olivero_breadcrumbs config:block.block.olivero_content config:block.block.olivero_help config:block.block.olivero_main_menu config:block.block.olivero_messages config:block.block.olivero_page_title config:block.block.olivero_powered config:block.block.olivero_primary_admin_actions config:block.block.olivero_primary_local_tasks config:block.block.olivero_search_form_narrow config:block.block.olivero_search_form_wide config:block.block.olivero_secondary_local_tasks config:block.block.olivero_site_branding config:block.block.olivero_syndicate config:block_list config:olark.settings config:system.menu.account config:system.menu.main config:system.site config:user.role.anonymous config:views.view.frontpage http_response local_task node_list rendered user:0

x-drupal-dynamic-cache: UNCACHEABLE

Bingo. The 'user' cache context was being set. But where? The next step was to fire up Xdebug and my IDE (VS Code) and to open /core/lib/Drupal/Core/Cache/Cache.php. This is where cache metadata gets merged together. In this case, I was interested in cache contexts. Specifically, I was interested in the 'user' cache context, so I temporary hacked core to set a breakpoint:

<?php
 
public static function mergeContexts(array ...$cache_contexts) {
+   foreach(
$cache_contexts as $context) {
+     if (
in_array('user', $context)) {
+      
$guilty_party = $context;    // <-- SET BREAKPOINT HERE
+     }
+   }
   
$cache_contexts = array_unique(array_merge(...$cache_contexts));
   
assert(\Drupal::service('cache_contexts_manager')->assertValidTokens($cache_contexts), sprintf('Failed to assert that "%s" are valid cache contexts.', implode(', ', $cache_contexts)));
    return
$cache_contexts;
  }
?>

As an anonymous user, when the breakpoint hit, I would know where the 'user' cache context was being set by looking at the trace:

Drupal\Core\Cache\Cache::mergeContexts (/Users/chris/Sites/drupal9-olark/web/core/lib/Drupal/Core/Cache/Cache.php:32)
Drupal\Core\Cache\CacheableMetadata->merge (/Users/chris/Sites/drupal9-olark/web/core/lib/Drupal/Core/Cache/CacheableMetadata.php:104)
Drupal\Core\Render\BubbleableMetadata->merge (/Users/chris/Sites/drupal9-olark/web/core/lib/Drupal/Core/Render/BubbleableMetadata.php:27)
Drupal\Core\Render\RenderContext->bubble (/Users/chris/Sites/drupal9-olark/web/core/lib/Drupal/Core/Render/RenderContext.php:53)
Drupal\Core\Render\Renderer->doRender (/Users/chris/Sites/drupal9-olark/web/core/lib/Drupal/Core/Render/Renderer.php:558) <-- STEP TO HERE
Drupal\Core\Render\Renderer->render (/Users/chris/Sites/drupal9-olark/web/core/lib/Drupal/Core/Render/Renderer.php:204)
Drupal\Core\Template\TwigExtension->escapeFilter (/Users/chris/Sites/drupal9-olark/web/core/lib/Drupal/Core/Template/TwigExtension.php:479)
...

When I stepped to Drupal\Core\Render\Renderer->doRender, the $keyvalue was 'olark'. When I expanded the $elementsarray, I found an 'olark' sub-array, which was what I was looking for:

Array
(
    [#markup] => Drupal\Core\Render\Markup Object
    [#attached] => Array
    [#cache] => Array
        (
            [contexts] => Array
                (
                    [0] => user  //    <-- HERE
                )

            [tags] => Array
                (
                    [0] => user:1
                    [1] => config:olark.settings
                )

            [max-age] => -1
        )
...

Olark is a module is that my client had me install after launch. I grep'd the module's code for '#cache' and found the module was setting the 'user' cache context for all users instead of just authenticated users in olark_page_bottom(). After patching the module, the issue was resolved.

Add new comment