diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php index 5b2eb9af33..6c99fb32a5 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -160,13 +160,18 @@ public function onRespond(FilterResponseEvent $event) { $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY); + $client_error_max_age = $this->config->get('cache.page.4xx_max_age'); + $max_age = $response->isClientError() && isset($client_error_max_age) + ? $client_error_max_age + : $this->config->get('cache.page.max_age'); + // Add headers necessary to specify whether the response should be cached by // proxies and/or the browser. - if ($is_cacheable && $this->config->get('cache.page.max_age') > 0) { + if ($is_cacheable && $max_age > 0) { if (!$this->isCacheControlCustomized($response)) { // Only add the default Cache-Control header if the controller did not // specify one on the response. - $this->setResponseCacheable($response, $request); + $this->setResponseCacheable($response, $request, $max_age); } } else { @@ -232,8 +237,10 @@ protected function setResponseNotCacheable(Response $response, Request $request) * A response object. * @param \Symfony\Component\HttpFoundation\Request $request * A request object. + * @param int $max_age + * The value for the max-age Cache-Control header */ - protected function setResponseCacheable(Response $response, Request $request) { + protected function setResponseCacheable(Response $response, Request $request, $max_age) { // HTTP/1.0 proxies do not support the Vary header, so prevent any caching // by sending an Expires date in the past. HTTP/1.1 clients ignore the // Expires header if a Cache-Control: max-age directive is specified (see @@ -242,7 +249,6 @@ protected function setResponseCacheable(Response $response, Request $request) { $this->setExpiresNoCache($response); } - $max_age = $this->config->get('cache.page.max_age'); $response->headers->set('Cache-Control', 'public, max-age=' . $max_age); // In order to support HTTP cache-revalidation, ensure that there is a diff --git a/core/modules/page_cache/tests/src/Functional/PageCacheTest.php b/core/modules/page_cache/tests/src/Functional/PageCacheTest.php index 95d101abcd..9fde4917cd 100644 --- a/core/modules/page_cache/tests/src/Functional/PageCacheTest.php +++ b/core/modules/page_cache/tests/src/Functional/PageCacheTest.php @@ -368,15 +368,20 @@ public function testPageCacheAnonymous403404() { 404 => $invalid_url, ]; $cache_ttl_4xx = Settings::get('cache_ttl_4xx', 3600); + $config = $this->config('system.performance'); foreach ($tests as $code => $content_url) { + $config->set('cache.page.4xx_max_age', 300)->save(); // Anonymous user, without permissions. $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); $this->assertCacheTag('4xx-response'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=300, public'); $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); + + // Saving an entity clears 4xx cache tag. $entity_values = [ 'name' => $this->randomMachineName(), 'user_id' => 1, @@ -389,18 +394,28 @@ public function testPageCacheAnonymous403404() { ]; $entity = EntityTest::create($entity_values); $entity->save(); - // Saving an entity clears 4xx cache tag. $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); + // Rebuilding the router should invalidate the 4xx cache tag. $this->container->get('router.builder')->rebuild(); $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + $this->drupalGet($content_url); + $this->assertSession()->statusCodeEquals($code); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); + + // Setting cache.page.4xx_max_age should invalidate the 4xx cache tag. + $config->set('cache.page.4xx_max_age', 0)->save(); + $this->drupalGet($content_url); + $this->assertSession()->statusCodeEquals($code); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, private'); // Ensure the 'expire' field on the cache entry uses cache_ttl_4xx. $cache_item = \Drupal::service('cache.page')->get($this->getUrl() . ':'); diff --git a/core/modules/system/config/install/system.performance.yml b/core/modules/system/config/install/system.performance.yml index 11392bd1e4..75c863efa4 100644 --- a/core/modules/system/config/install/system.performance.yml +++ b/core/modules/system/config/install/system.performance.yml @@ -1,6 +1,7 @@ cache: page: max_age: 0 + 4xx_max_age: null css: preprocess: true gzip: true diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index e68bbae5a8..a1da025478 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -152,6 +152,10 @@ system.performance: max_age: type: integer label: 'Max age' + 4xx_max_age: + type: integer + nullable: true + label: 'Max age for 4xx responses' css: type: mapping label: 'CSS performance settings' diff --git a/core/modules/system/src/SystemConfigSubscriber.php b/core/modules/system/src/SystemConfigSubscriber.php index 0ab0d15fc6..d9bfe88c8d 100644 --- a/core/modules/system/src/SystemConfigSubscriber.php +++ b/core/modules/system/src/SystemConfigSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\system; +use Drupal\Core\Cache\Cache; use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigImporterEvent; @@ -42,6 +43,9 @@ public function onConfigSave(ConfigCrudEvent $event) { if ($saved_config->getName() == 'system.theme' && ($event->isChanged('admin') || $event->isChanged('default'))) { $this->routerBuilder->setRebuildNeeded(); } + if ($saved_config->getName() === 'system.performance' && $event->isChanged('cache.page.4xx_max_age')) { + Cache::invalidateTags(['4xx-response']); + } } /** diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index 242ad9d1b5..353e9ea3c8 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -267,3 +267,12 @@ function system_post_update_entity_reference_autocomplete_match_limit(&$sandbox $config_entity_updater->update($sandbox, 'entity_form_display', $callback); } + +/** + * Set max-age for 4xx responses to null. + */ +function system_post_update_set_max_age_4xx() { + \Drupal::configFactory()->getEditable('system.performance') + ->set('cache.page.4xx_max_age', NULL) + ->save(TRUE); +} diff --git a/core/modules/system/tests/src/Functional/Update/AddPageCache4xxMaxAgeToSystemPerformanceConfigurationTest.php b/core/modules/system/tests/src/Functional/Update/AddPageCache4xxMaxAgeToSystemPerformanceConfigurationTest.php new file mode 100644 index 0000000000..75e15b1873 --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/AddPageCache4xxMaxAgeToSystemPerformanceConfigurationTest.php @@ -0,0 +1,37 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../fixtures/update/drupal-8.8.0.bare.standard.php.gz', + ]; + } + + /** + * Ensures cache.page.4xx_max_age is added to system.performance + * configuration. + */ + public function testUpdate() { + $system_performance = \Drupal::config('system.performance')->get(); + $this->assertArrayNotHasKey('4xx_max_age', $system_performance['cache']['page'], 'Configuration cache.page.4xx_max_age does not exist in system.performance.'); + + $this->runUpdates(); + + $system_performance = \Drupal::config('system.performance')->get(); + $this->assertArrayHasKey('4xx_max_age', $system_performance['cache']['page'], 'Configuration cache.page.4xx_max_age has been added to system.performance.'); + } + +} diff --git a/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php index f1c62a481a..5b8241a61f 100644 --- a/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php +++ b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php @@ -68,6 +68,7 @@ class MigrateSystemConfigurationTest extends MigrateDrupal6TestBase { 'cache' => [ 'page' => [ 'max_age' => 0, + '4xx_max_age' => NULL, ], ], 'css' => [ diff --git a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php index 8fef991b4f..c1aa2f0910 100644 --- a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php +++ b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php @@ -71,6 +71,7 @@ class MigrateSystemConfigurationTest extends MigrateDrupal7TestBase { 'cache' => [ 'page' => [ 'max_age' => 300, + '4xx_max_age' => NULL, ], ], 'css' => [