From 172e9e38cc95a7aa1fabd4a2b7122a7b4b5d6764 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 10 Feb 2017 10:45:01 +0000
Subject: [PATCH] Issue #2815845 by dawehner, Wim Leers, alexpott, tedbow,
 Berdir, swentel, webflo: Importing (deploying) REST resource config entities
 should automatically do the necessary route rebuilding

---
 .../CacheRouterRebuildSubscriber.php          |  4 +-
 .../FinishResponseSubscriber.php              | 18 +++++++
 core/modules/block/src/Tests/BlockTest.php    |  2 +
 .../src/Tests/Views/DisplayBlockTest.php      | 10 ++--
 .../menu_ui/src/Tests/MenuCacheTagsTest.php   |  3 +-
 .../node/src/Tests/Views/FrontPageTest.php    |  5 +-
 .../Tests/PageCacheTagsIntegrationTest.php    |  2 +
 .../page_cache/src/Tests/PageCacheTest.php    |  2 +
 core/modules/rest/rest.services.yml           |  7 +++
 .../rest/src/Entity/RestResourceConfig.php    | 19 +++++++
 .../EventSubscriber/RestConfigSubscriber.php  | 53 +++++++++++++++++++
 core/modules/rest/src/Tests/RESTTestBase.php  |  3 +-
 .../Block/BlockResourceTestBase.php           |  4 +-
 .../EntityResource/EntityResourceTestBase.php | 39 ++++++++++----
 .../tests/src/Functional/ResourceTestBase.php | 22 +++++++-
 .../src/Tests/SearchPageCacheTagsTest.php     |  9 ++--
 .../AssertPageCacheContextsAndTagsTrait.php   |  8 ++-
 .../Tests/Entity/EntityCacheTagsTestBase.php  |  4 +-
 .../system/src/Tests/Routing/RouterTest.php   | 10 ++--
 .../tour/src/Tests/TourCacheTagsTest.php      |  2 +
 core/modules/views/src/Tests/GlossaryTest.php |  1 +
 .../FunctionalTests/BrowserTestBaseTest.php   |  2 +-
 22 files changed, 188 insertions(+), 41 deletions(-)
 create mode 100644 core/modules/rest/src/EventSubscriber/RestConfigSubscriber.php

diff --git a/core/lib/Drupal/Core/EventSubscriber/CacheRouterRebuildSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/CacheRouterRebuildSubscriber.php
index 38007f56003d..c1f532799efc 100644
--- a/core/lib/Drupal/Core/EventSubscriber/CacheRouterRebuildSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/CacheRouterRebuildSubscriber.php
@@ -16,8 +16,8 @@ class CacheRouterRebuildSubscriber implements EventSubscriberInterface {
    */
   public function onRouterFinished() {
     // Requested URLs that formerly gave a 403/404 may now be valid.
-    // Also invalidate all cached routing.
-    Cache::invalidateTags(['4xx-response', 'route_match']);
+    // Also invalidate all cached routing as well as every HTTP response.
+    Cache::invalidateTags(['4xx-response', 'route_match', 'http_response']);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 3db181adc81c..5898d17ae6a4 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -88,6 +88,21 @@ public function __construct(LanguageManagerInterface $language_manager, ConfigFa
     $this->debugCacheabilityHeaders = $http_response_debug_cacheability_headers;
   }
 
+  /**
+   * Sets extra headers on any responses, also subrequest ones.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onAllResponds(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+    // Always add the 'http_response' cache tag to be able to invalidate every
+    // response, for example after rebuilding routes.
+    if ($response instanceof CacheableResponseInterface) {
+      $response->getCacheableMetadata()->addCacheTags(['http_response']);
+    }
+  }
+
   /**
    * Sets extra headers on successful responses.
    *
@@ -284,6 +299,9 @@ protected function setExpiresNoCache(Response $response) {
    */
   public static function getSubscribedEvents() {
     $events[KernelEvents::RESPONSE][] = array('onRespond');
+    // There is no specific reason for choosing 16 beside it should be executed
+    // before ::onRespond().
+    $events[KernelEvents::RESPONSE][] = array('onAllResponds', 16);
     return $events;
   }
 
diff --git a/core/modules/block/src/Tests/BlockTest.php b/core/modules/block/src/Tests/BlockTest.php
index 9fd869a92c59..57f46476445e 100644
--- a/core/modules/block/src/Tests/BlockTest.php
+++ b/core/modules/block/src/Tests/BlockTest.php
@@ -385,6 +385,7 @@ public function testBlockCacheTags() {
       'block_view',
       'config:block.block.powered',
       'config:user.role.anonymous',
+      'http_response',
       'rendered',
     );
     sort($expected_cache_tags);
@@ -426,6 +427,7 @@ public function testBlockCacheTags() {
       'config:block.block.powered',
       'config:block.block.powered-2',
       'config:user.role.anonymous',
+      'http_response',
       'rendered',
     );
     sort($expected_cache_tags);
diff --git a/core/modules/block/src/Tests/Views/DisplayBlockTest.php b/core/modules/block/src/Tests/Views/DisplayBlockTest.php
index 368af68ad9f1..d87817526eb7 100644
--- a/core/modules/block/src/Tests/Views/DisplayBlockTest.php
+++ b/core/modules/block/src/Tests/Views/DisplayBlockTest.php
@@ -262,7 +262,7 @@ public function testBlockRendering() {
     $result = $this->xpath('//div[contains(@class, "region-sidebar-first")]/div[contains(@class, "block-views")]/h2');
     $this->assertTrue(empty($result), 'The title is not visible.');
 
-    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' , 'rendered']));
+    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' , 'http_response', 'rendered']));
   }
 
   /**
@@ -288,7 +288,7 @@ public function testBlockEmptyRendering() {
     $this->assertEqual(0, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
     // Ensure that the view cachability metadata is propagated even, for an
     // empty block.
-    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'rendered']));
+    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'http_response', 'rendered']));
     $this->assertCacheContexts(['url.query_args:_wrapper_format']);
 
     // Add a header displayed on empty result.
@@ -306,7 +306,7 @@ public function testBlockEmptyRendering() {
 
     $this->drupalGet($url);
     $this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
-    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'rendered']));
+    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'http_response', 'rendered']));
     $this->assertCacheContexts(['url.query_args:_wrapper_format']);
 
     // Hide the header on empty results.
@@ -324,7 +324,7 @@ public function testBlockEmptyRendering() {
 
     $this->drupalGet($url);
     $this->assertEqual(0, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
-    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'rendered']));
+    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block', 'http_response', 'rendered']));
     $this->assertCacheContexts(['url.query_args:_wrapper_format']);
 
     // Add an empty text.
@@ -341,7 +341,7 @@ public function testBlockEmptyRendering() {
 
     $this->drupalGet($url);
     $this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
-    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'rendered']));
+    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block', 'http_response', 'rendered']));
     $this->assertCacheContexts(['url.query_args:_wrapper_format']);
   }
 
diff --git a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php
index 3ad4ad1d40db..bba47d5cc76c 100644
--- a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php
+++ b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php
@@ -46,6 +46,7 @@ public function testMenuBlock() {
 
     // Verify a cache hit, but also the presence of the correct cache tags.
     $expected_tags = array(
+      'http_response',
       'rendered',
       'block_view',
       'config:block_list',
@@ -107,7 +108,7 @@ public function testMenuBlock() {
     $this->verifyPageCache($url, 'MISS');
 
     // Verify a cache hit.
-    $this->verifyPageCache($url, 'HIT', ['config:block_list', 'config:user.role.anonymous', 'rendered']);
+    $this->verifyPageCache($url, 'HIT', ['config:block_list', 'config:user.role.anonymous', 'http_response', 'rendered']);
   }
 
 }
diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index b27c416cb655..19cb659da24a 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -265,7 +265,7 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
       $render_cache_tags
     );
     $expected_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags);
-    $expected_tags = Cache::mergeTags($expected_tags, ['rendered', 'config:user.role.anonymous', 'config:system.site']);
+    $expected_tags = Cache::mergeTags($expected_tags, ['http_response', 'rendered', 'config:user.role.anonymous', 'config:system.site']);
     $this->assertPageCacheContextsAndTags(
       Url::fromRoute('view.frontpage.page_1'),
       $cache_contexts,
@@ -331,7 +331,7 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
     $this->assertPageCacheContextsAndTags(
       Url::fromRoute('view.frontpage.page_1'),
       $cache_contexts,
-      Cache::mergeTags($first_page_output_cache_tags, ['rendered', 'config:user.role.anonymous'])
+      Cache::mergeTags($first_page_output_cache_tags, ['http_response', 'rendered', 'config:user.role.anonymous'])
     );
 
     // Second page.
@@ -350,6 +350,7 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
       'node_view',
       'user_view',
       'user:0',
+      'http_response',
       'rendered',
       // FinishResponseSubscriber adds this cache tag to responses that have the
       // 'user.permissions' cache context for anonymous users.
diff --git a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
index f9fd3de57192..2c945fdb3d7a 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
@@ -80,6 +80,7 @@ function testPageCacheTags() {
 
     // Full node page 1.
     $this->assertPageCacheContextsAndTags($node_1->urlInfo(), $cache_contexts, array(
+      'http_response',
       'rendered',
       'block_view',
       'config:block_list',
@@ -120,6 +121,7 @@ function testPageCacheTags() {
 
     // Full node page 2.
     $this->assertPageCacheContextsAndTags($node_2->urlInfo(), $cache_contexts, array(
+      'http_response',
       'rendered',
       'block_view',
       'config:block_list',
diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php
index 3a7cd17cd4ba..d4fae54a5fca 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTest.php
@@ -63,6 +63,7 @@ function testPageCacheTags() {
     sort($cache_entry->tags);
     $expected_tags = array(
       'config:user.role.anonymous',
+      'http_response',
       'pre_render',
       'rendered',
       'system_test_cache_tags_page',
@@ -94,6 +95,7 @@ function testPageCacheTagsIndependentFromCacheabilityHeaders() {
     sort($cache_entry->tags);
     $expected_tags = array(
       'config:user.role.anonymous',
+      'http_response',
       'pre_render',
       'rendered',
       'system_test_cache_tags_page',
diff --git a/core/modules/rest/rest.services.yml b/core/modules/rest/rest.services.yml
index 2def91e992d1..7742e1dbc7a7 100644
--- a/core/modules/rest/rest.services.yml
+++ b/core/modules/rest/rest.services.yml
@@ -35,8 +35,15 @@ services:
   logger.channel.rest:
     parent: logger.channel_base
     arguments: ['rest']
+
+  # Event subscribers.
   rest.resource_response.subscriber:
     class: Drupal\rest\EventSubscriber\ResourceResponseSubscriber
     tags:
       - { name: event_subscriber }
     arguments: ['@serializer', '@renderer', '@current_route_match']
+  rest.config_subscriber:
+    class: Drupal\rest\EventSubscriber\RestConfigSubscriber
+    arguments: ['@router.builder']
+    tags:
+      - { name: event_subscriber }
diff --git a/core/modules/rest/src/Entity/RestResourceConfig.php b/core/modules/rest/src/Entity/RestResourceConfig.php
index bb835841610e..12bd846ea6a6 100644
--- a/core/modules/rest/src/Entity/RestResourceConfig.php
+++ b/core/modules/rest/src/Entity/RestResourceConfig.php
@@ -3,6 +3,7 @@
 namespace Drupal\rest\Entity;
 
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
 use Drupal\rest\RestResourceConfigInterface;
 
@@ -255,4 +256,22 @@ protected function normalizeRestMethod($method) {
     return strtoupper($method);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+
+    \Drupal::service('router.builder')->setRebuildNeeded();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageInterface $storage, array $entities) {
+    parent::postDelete($storage, $entities);
+
+    \Drupal::service('router.builder')->setRebuildNeeded();
+  }
+
 }
diff --git a/core/modules/rest/src/EventSubscriber/RestConfigSubscriber.php b/core/modules/rest/src/EventSubscriber/RestConfigSubscriber.php
new file mode 100644
index 000000000000..abab34a86eda
--- /dev/null
+++ b/core/modules/rest/src/EventSubscriber/RestConfigSubscriber.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\rest\EventSubscriber;
+
+use Drupal\Core\Config\ConfigCrudEvent;
+use Drupal\Core\Config\ConfigEvents;
+use Drupal\Core\Routing\RouteBuilderInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * A subscriber triggering a route rebuild when certain configuration changes.
+ */
+class RestConfigSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The router builder.
+   *
+   * @var \Drupal\Core\Routing\RouteBuilderInterface
+   */
+  protected $routerBuilder;
+
+  /**
+   * Constructs the RestConfigSubscriber.
+   *
+   * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
+   *   The router builder service.
+   */
+  public function __construct(RouteBuilderInterface $router_builder) {
+    $this->routerBuilder = $router_builder;
+  }
+
+  /**
+   * Informs the router builder a rebuild is needed when necessary.
+   *
+   * @param \Drupal\Core\Config\ConfigCrudEvent $event
+   *   The Event to process.
+   */
+  public function onSave(ConfigCrudEvent $event) {
+    $saved_config = $event->getConfig();
+    if ($saved_config->getName() === 'rest.settings' && $event->isChanged('bc_entity_resource_permissions')) {
+      $this->routerBuilder->setRebuildNeeded();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[ConfigEvents::SAVE][] = ['onSave'];
+    return $events;
+  }
+
+}
diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php
index 271df515f5ca..405493bccb71 100644
--- a/core/modules/rest/src/Tests/RESTTestBase.php
+++ b/core/modules/rest/src/Tests/RESTTestBase.php
@@ -420,8 +420,7 @@ protected function enableService($resource_type, $method = 'GET', $format = NULL
    * Rebuilds routing caches.
    */
   protected function rebuildCache() {
-    // Rebuild routing cache, so that the REST API paths are available.
-    $this->container->get('router.builder')->rebuild();
+    $this->container->get('router.builder')->rebuildIfNeeded();
   }
 
   /**
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
index d87e42345c69..0be7622068c0 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
@@ -122,9 +122,9 @@ protected function getExpectedCacheContexts() {
   protected function getExpectedCacheTags() {
     // Because the 'user.permissions' cache context is missing, the cache tag
     // for the anonymous user role is never added automatically.
-    return array_filter(parent::getExpectedCacheTags(), function ($tag) {
+    return array_values(array_filter(parent::getExpectedCacheTags(), function ($tag) {
       return $tag !== 'config:user.role.anonymous';
-    });
+    }));
   }
 
   /**
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 2c9561239580..992e3b0bf17a 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -146,6 +146,15 @@ protected function provisionEntityResource() {
     $this->provisionResource('entity.' . static::$entityTypeId, [static::$format], $auth);
   }
 
+  /**
+   * Deprovisions the tested entity resource.
+   */
+  protected function deprovisionEntityResource() {
+    $this->resourceConfigStorage->load('entity.' . static::$entityTypeId)
+      ->delete();
+    $this->refreshTestStateAfterRestConfigChange();
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -195,9 +204,6 @@ public function setUp() {
       }
       $this->entity->save();
     }
-
-    // @todo Remove this in https://www.drupal.org/node/2815845.
-    drupal_flush_all_caches();
   }
 
   /**
@@ -291,6 +297,7 @@ protected function getExpectedCacheTags() {
     if (!static::$auth) {
       $expected_cache_tags[] = 'config:user.role.anonymous';
     }
+    $expected_cache_tags[] = 'http_response';
     return Cache::mergeTags($expected_cache_tags, $this->entity->getCacheTags());
   }
 
@@ -458,8 +465,7 @@ public function testGet() {
 
 
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
-    // @todo Remove this in https://www.drupal.org/node/2815845.
-    drupal_flush_all_caches();
+    $this->refreshTestStateAfterRestConfigChange();
 
 
     // DX: 403 when unauthorized.
@@ -475,6 +481,20 @@ public function testGet() {
     $this->assertResourceResponse(200, FALSE, $response);
 
 
+    $this->deprovisionEntityResource();
+
+
+    // DX: upon deprovisioning, immediate 404 if no route, 406 otherwise.
+    $response = $this->request('GET', $url, $request_options);
+    if (!$has_canonical_url) {
+      $this->assertSame(404, $response->getStatusCode());
+    }
+    else {
+      $this->assert406Response($response);
+    }
+
+
+    $this->provisionEntityResource();
     $url->setOption('query', ['_format' => 'non_existing_format']);
 
 
@@ -673,9 +693,8 @@ public function testPost() {
 
 
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
+    $this->refreshTestStateAfterRestConfigChange();
     $request_options[RequestOptions::BODY] = $parseable_valid_request_body_2;
-    // @todo Remove this in https://www.drupal.org/node/2815845.
-    drupal_flush_all_caches();
 
 
     // DX: 403 when unauthorized.
@@ -876,9 +895,8 @@ public function testPatch() {
 
 
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
+    $this->refreshTestStateAfterRestConfigChange();
     $request_options[RequestOptions::BODY] = $parseable_valid_request_body_2;
-    // @todo Remove this in https://www.drupal.org/node/2815845.
-    drupal_flush_all_caches();
 
 
     // DX: 403 when unauthorized.
@@ -975,8 +993,7 @@ public function testDelete() {
 
 
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
-    // @todo Remove this in https://www.drupal.org/node/2815845.
-    drupal_flush_all_caches();
+    $this->refreshTestStateAfterRestConfigChange();
     $this->entity = $this->createEntity();
     $url = $this->getUrl()->setOption('query', $url->getOption('query'));
 
diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
index 2674d705ecbc..d0f601b634a4 100644
--- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
@@ -119,6 +119,7 @@ public function setUp() {
 
     // Ensure there's a clean slate: delete all REST resource config entities.
     $this->resourceConfigStorage->delete($this->resourceConfigStorage->loadMultiple());
+    $this->refreshTestStateAfterRestConfigChange();
   }
 
   /**
@@ -141,8 +142,25 @@ protected function provisionResource($resource_type, $formats = [], $authenticat
         'authentication' => $authentication,
       ]
     ])->save();
-    // @todo Remove this in https://www.drupal.org/node/2815845.
-    drupal_flush_all_caches();
+    $this->refreshTestStateAfterRestConfigChange();
+  }
+
+  /**
+   * Refreshes the state of the tester to be in sync with the testee.
+   *
+   * Should be called after every change made to:
+   * - RestResourceConfig entities
+   * - the 'rest.settings' simple configuration
+   */
+  protected function refreshTestStateAfterRestConfigChange() {
+    // Ensure that the cache tags invalidator has its internal values reset.
+    // Otherwise the http_response cache tag invalidation won't work.
+    $this->refreshVariables();
+
+    // Tests using this base class may trigger route rebuilds due to changes to
+    // RestResourceConfig entities or 'rest.settings'. Ensure the test generates
+    // routes using an up-to-date router.
+    \Drupal::service('router.builder')->rebuildIfNeeded();
   }
 
   /**
diff --git a/core/modules/search/src/Tests/SearchPageCacheTagsTest.php b/core/modules/search/src/Tests/SearchPageCacheTagsTest.php
index 3a7bc67e745e..667b2135f866 100644
--- a/core/modules/search/src/Tests/SearchPageCacheTagsTest.php
+++ b/core/modules/search/src/Tests/SearchPageCacheTagsTest.php
@@ -70,6 +70,7 @@ function testSearchText() {
     $this->assertCacheTag('node:1');
     $this->assertCacheTag('user:2');
     $this->assertCacheTag('rendered');
+    $this->assertCacheTag('http_response');
     $this->assertCacheTag('node_list');
 
     // Updating a node should invalidate the search plugin's index cache tag.
@@ -83,6 +84,7 @@ function testSearchText() {
     $this->assertCacheTag('node:1');
     $this->assertCacheTag('user:2');
     $this->assertCacheTag('rendered');
+    $this->assertCacheTag('http_response');
     $this->assertCacheTag('node_list');
 
     // Deleting a node should invalidate the search plugin's index cache tag.
@@ -172,6 +174,7 @@ public function testSearchTagsBubbling() {
       'config:search.page.node_search',
       'search_index',
       'search_index:node_search',
+      'http_response',
       'rendered',
       'node_list',
     ];
@@ -192,8 +195,7 @@ public function testSearchTagsBubbling() {
       'node_view',
       'config:filter.format.plain_text',
     ]);
-    $cache_tags = $this->drupalGetHeader('X-Drupal-Cache-Tags');
-    $this->assertEqual(explode(' ', $cache_tags), $expected_cache_tags);
+    $this->assertCacheTags($expected_cache_tags);
 
     // Only get the new node in the search results, should result in node:1,
     // node:2 and user:3 as cache tags even though only node:1 is shown. This is
@@ -208,8 +210,7 @@ public function testSearchTagsBubbling() {
       'user:3',
       'node_view',
     ]);
-    $cache_tags = $this->drupalGetHeader('X-Drupal-Cache-Tags');
-    $this->assertEqual(explode(' ', $cache_tags), $expected_cache_tags);
+    $this->assertCacheTags($expected_cache_tags);
   }
 
 }
diff --git a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
index 94ea1ac44c5b..c216e9bcfd53 100644
--- a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
+++ b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
@@ -122,10 +122,14 @@ protected function debugCacheTags(array $actual_tags, array $expected_tags) {
    */
   protected function assertCacheTags(array $expected_tags, $include_default_tags = TRUE) {
     // The anonymous role cache tag is only added if the user is anonymous.
-    if ($include_default_tags && \Drupal::currentUser()->isAnonymous()) {
-      $expected_tags = Cache::mergeTags($expected_tags, ['config:user.role.anonymous']);
+    if ($include_default_tags) {
+      if (\Drupal::currentUser()->isAnonymous()) {
+        $expected_tags = Cache::mergeTags($expected_tags, ['config:user.role.anonymous']);
+      }
+      $expected_tags[] = 'http_response';
     }
     $actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags');
+    $expected_tags = array_unique($expected_tags);
     sort($expected_tags);
     sort($actual_tags);
     $this->assertIdentical($actual_tags, $expected_tags);
diff --git a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php
index 9ecefad4fdcd..c5e9a95e620c 100644
--- a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php
+++ b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php
@@ -343,7 +343,7 @@ public function testReferencedEntity() {
     // 'user.permissions' is a required cache context, and responses that vary
     // by this cache context when requested by anonymous users automatically
     // also get this cache tag, to ensure correct invalidation.
-    $page_cache_tags = Cache::mergeTags(['rendered'], ['config:user.role.anonymous']);
+    $page_cache_tags = Cache::mergeTags(['http_response', 'rendered'], ['config:user.role.anonymous']);
     // If the block module is used, the Block page display variant is used,
     // which adds the block config entity type's list cache tags.
     $page_cache_tags = Cache::mergeTags($page_cache_tags, \Drupal::moduleHandler()->moduleExists('block') ? ['config:block_list'] : []);
@@ -641,7 +641,7 @@ public function testReferencedEntity() {
 
     // Verify cache hits.
     $referencing_entity_cache_tags = Cache::mergeTags($this->referencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags());
-    $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['rendered']);
+    $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['http_response', 'rendered']);
 
     $nonempty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->getAdditionalCacheTagsForEntityListing());
     $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $page_cache_tags);
diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php
index 1b33bafdc747..c6aa2b8bc9b5 100644
--- a/core/modules/system/src/Tests/Routing/RouterTest.php
+++ b/core/modules/system/src/Tests/Routing/RouterTest.php
@@ -45,7 +45,7 @@ public function testFinishResponseSubscriber() {
     // Check expected headers from FinishResponseSubscriber.
     $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', $expected_cache_contexts));
-    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous rendered');
+    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous http_response rendered');
     // Confirm that the page wrapping is being added, so we're not getting a
     // raw body returned.
     $this->assertRaw('</html>', 'Page markup was found.');
@@ -60,12 +60,12 @@ public function testFinishResponseSubscriber() {
     $this->drupalGet('router_test/test18');
     $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url'])));
-    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo rendered');
+    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo http_response rendered');
     // 2. controller result: render array, per-role cacheable route access.
     $this->drupalGet('router_test/test19');
     $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url', 'user.roles'])));
-    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo rendered');
+    $this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo http_response rendered');
     // 3. controller result: Response object, globally cacheable route access.
     $this->drupalGet('router_test/test1');
     $headers = $this->drupalGetHeaders();
@@ -80,12 +80,12 @@ public function testFinishResponseSubscriber() {
     $this->drupalGet('router_test/test21');
     $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-drupal-cache-contexts'], '');
-    $this->assertEqual($headers['x-drupal-cache-tags'], '');
+    $this->assertEqual($headers['x-drupal-cache-tags'], 'http_response');
     // 6. controller result: CacheableResponse object, per-role cacheable route access.
     $this->drupalGet('router_test/test22');
     $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles');
-    $this->assertEqual($headers['x-drupal-cache-tags'], '');
+    $this->assertEqual($headers['x-drupal-cache-tags'], 'http_response');
 
     // 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.
diff --git a/core/modules/tour/src/Tests/TourCacheTagsTest.php b/core/modules/tour/src/Tests/TourCacheTagsTest.php
index 4f2164e1884c..7a2e87d1c155 100644
--- a/core/modules/tour/src/Tests/TourCacheTagsTest.php
+++ b/core/modules/tour/src/Tests/TourCacheTagsTest.php
@@ -48,6 +48,7 @@ public function testRenderedTour() {
     $expected_tags = [
       'config:tour.tour.tour-test',
       'config:user.role.anonymous',
+      'http_response',
       'rendered',
     ];
     $this->verifyPageCache($url, 'HIT', $expected_tags);
@@ -68,6 +69,7 @@ public function testRenderedTour() {
     // Verify a cache hit.
     $expected_tags = [
       'config:user.role.anonymous',
+      'http_response',
       'rendered',
     ];
     $this->verifyPageCache($url, 'HIT', $expected_tags);
diff --git a/core/modules/views/src/Tests/GlossaryTest.php b/core/modules/views/src/Tests/GlossaryTest.php
index 508a4ebc023e..2a5bc907c6ec 100644
--- a/core/modules/views/src/Tests/GlossaryTest.php
+++ b/core/modules/views/src/Tests/GlossaryTest.php
@@ -95,6 +95,7 @@ public function testGlossaryView() {
         'node_list',
         'user:0',
         'user_list',
+        'http_response',
         'rendered',
         // FinishResponseSubscriber adds this cache tag to responses that have the
         // 'user.permissions' cache context for anonymous users.
diff --git a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
index 199d25774d1b..25d1c702eb43 100644
--- a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
+++ b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
@@ -43,7 +43,7 @@ public function testGoTo() {
     $this->assertNotContains('</html>', $text);
 
     // Response includes cache tags that we can assert.
-    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', 'rendered');
+    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', 'http_response rendered');
 
     // Test that we can read the JS settings.
     $js_settings = $this->getDrupalSettings();
-- 
GitLab