diff --git a/core/core.services.yml b/core/core.services.yml index e39252d..dd5c4fd 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -16,6 +16,7 @@ parameters: tags: [] factory.keyvalue: default: keyvalue.database + http.response.debug_cacheability_headers: false factory.keyvalue.expirable: default: keyvalue.expirable.database filter_protocols: @@ -1070,7 +1071,7 @@ services: class: Drupal\Core\EventSubscriber\FinishResponseSubscriber tags: - { name: event_subscriber } - arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy', '@cache_contexts_manager'] + arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy', '@cache_contexts_manager', '%http.response.debug_cacheability_headers%'] response_generator_subscriber: class: Drupal\Core\EventSubscriber\ResponseGeneratorSubscriber tags: diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php index b625f59..a373688 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -62,6 +62,13 @@ class FinishResponseSubscriber implements EventSubscriberInterface { protected $cacheContexts; /** + * Whether to send cacheability headers for debugging purposes. + * + * @var bool + */ + protected $debugCacheabilityHeaders = FALSE; + + /** * Constructs a FinishResponseSubscriber object. * * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager @@ -74,13 +81,16 @@ class FinishResponseSubscriber implements EventSubscriberInterface { * A policy rule determining the cacheability of a response. * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager * The cache contexts manager service. + * @param bool $http_response_debug_cacheability_headers + * (optional) Whether to send cacheability headers for debugging purposes. */ - public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, CacheContextsManager $cache_contexts_manager) { + public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, CacheContextsManager $cache_contexts_manager, $http_response_debug_cacheability_headers = FALSE) { $this->languageManager = $language_manager; $this->config = $config_factory->get('system.performance'); $this->requestPolicy = $request_policy; $this->responsePolicy = $response_policy; $this->cacheContextsManager = $cache_contexts_manager; + $this->debugCacheabilityHeaders = $http_response_debug_cacheability_headers; } /** @@ -130,11 +140,13 @@ public function onRespond(FilterResponseEvent $event) { return; } - // Expose the cache contexts and cache tags associated with this page in a - // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. - $response_cacheability = $response->getCacheableMetadata(); - $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags())); - $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts()))); + if ($this->debugCacheabilityHeaders) { + // Expose the cache contexts and cache tags associated with this page in a + // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. + $response_cacheability = $response->getCacheableMetadata(); + $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags())); + $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts()))); + } $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY); diff --git a/core/modules/page_cache/src/StackMiddleware/PageCache.php b/core/modules/page_cache/src/StackMiddleware/PageCache.php index 23ddb84..85a7275 100644 --- a/core/modules/page_cache/src/StackMiddleware/PageCache.php +++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php @@ -8,6 +8,7 @@ namespace Drupal\page_cache\StackMiddleware; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\PageCache\RequestPolicyInterface; use Drupal\Core\PageCache\ResponsePolicyInterface; @@ -224,7 +225,7 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch $date = $response->getExpires()->getTimestamp(); $expire = ($date > time()) ? $date : Cache::PERMANENT; - $tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags')); + $tags = ($response instanceof CacheableResponseInterface) ? $response->getCacheableMetadata()->getCacheTags() : []; $this->set($request, $response, $expire, $tags); // Mark response as a cache miss. diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index a2278e7..262ab4c 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -80,6 +80,37 @@ function testPageCacheTags() { } /** + * Test that the page cache doesn't depend on cacheability headers. + */ + function testPageCacheTagsIndependentFromCacheabilityHeaders() { + $this->setHttpResponseDebugCacheabilityHeaders(FALSE); + + $path = 'system-test/cache_tags_page'; + $tags = array('system_test_cache_tags_page'); + $this->drupalGet($path); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + + // Verify a cache hit, but also the presence of the correct cache tags. + $this->drupalGet($path); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); + $cid_parts = array(\Drupal::url('system_test.cache_tags_page', array(), array('absolute' => TRUE)), 'html'); + $cid = implode(':', $cid_parts); + $cache_entry = \Drupal::cache('render')->get($cid); + sort($cache_entry->tags); + $expected_tags = array( + 'config:user.role.anonymous', + 'pre_render', + 'rendered', + 'system_test_cache_tags_page', + ); + $this->assertIdentical($cache_entry->tags, $expected_tags); + + Cache::invalidateTags($tags); + $this->drupalGet($path); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + } + + /** * Tests support for different cache items with different request formats * specified via a query parameter. */ diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 5b62c50..ccdeded 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -812,6 +812,10 @@ protected function initSettings() { // TestBase::restoreEnvironment() will delete the entire site directory. // Not using File API; a potential error must trigger a PHP warning. chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777); + + // During tests, cacheable responses should get the debugging cacheability + // headers by default. + $this->setContainerParameter('http.response.debug_cacheability_headers', TRUE); } /** @@ -3019,4 +3023,18 @@ protected function assertNoCacheTag($cache_tag) { $this->assertFalse(in_array($cache_tag, $cache_tags), "'" . $cache_tag . "' is absent in the X-Drupal-Cache-Tags header."); } + /** + * Enables/disables the cacheability headers. + * + * Sets the http.response.debug_cacheability_headers container parameter. + * + * @param bool $value + * (optional) Whether the debugging cacheability headers should be sent. + */ + protected function setHttpResponseDebugCacheabilityHeaders($value = TRUE) { + $this->setContainerParameter('http.response.debug_cacheability_headers', $value); + $this->rebuildContainer(); + $this->resetAll(); + } + } diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php index 17345fa..5db3230 100644 --- a/core/modules/system/src/Tests/Routing/RouterTest.php +++ b/core/modules/system/src/Tests/Routing/RouterTest.php @@ -91,6 +91,18 @@ public function testFinishResponseSubscriber() { $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles'); $this->assertEqual($headers['x-drupal-cache-tags'], ''); + + // Finally, verify that the X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags + // headers are not sent when their container parameter is set to FALSE. + $this->drupalGet('router_test/test18'); + $headers = $this->drupalGetHeaders(); + $this->assertTrue(isset($headers['x-drupal-cache-contexts'])); + $this->assertTrue(isset($headers['x-drupal-cache-tags'])); + $this->setHttpResponseDebugCacheabilityHeaders(FALSE); + $this->drupalGet('router_test/test18'); + $headers = $this->drupalGetHeaders(); + $this->assertFalse(isset($headers['x-drupal-cache-contexts'])); + $this->assertFalse(isset($headers['x-drupal-cache-tags'])); } /** diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml index 4ab0662..23f6483 100644 --- a/sites/default/default.services.yml +++ b/sites/default/default.services.yml @@ -115,6 +115,17 @@ parameters: # # @default [] tags: [] + # Cacheability debugging: + # + # Responses with cacheability metadata (CacheableResponseInterface instances) + # get X-Drupal-Cache-Tags and X-Drupal-Cache-Contexts headers. + # + # For more information about debugging cacheable responses, see + # https://www.drupal.org/developing/api/8/response/cacheable-response-interface + # + # Not recommended in production environments + # @default false + http.response.debug_cacheability_headers: false factory.keyvalue: {} # Default key/value storage service to use.