 .../Core/Cache/CacheableResponseInterface.php      |   1 +
 .../Drupal/Core/Cache/CacheableResponseTrait.php   |   3 +-
 .../CacheableAccessDeniedHttpException.php         |  15 +++
 .../Exception/CacheableBadRequestHttpException.php |  15 +++
 .../Exception/CacheableConflictHttpException.php   |  15 +++
 .../Cache/Exception/CacheableGoneHttpException.php |  15 +++
 .../Cache/Exception/CacheableHttpException.php     |  15 +++
 .../Exception/CacheableHttpExceptionInterface.php  |  40 ++++++++
 .../CacheableLengthRequiredHttpException.php       |  15 +++
 .../CacheableMethodNotAllowedHttpException.php     |  15 +++
 .../CacheableNotAcceptableHttpException.php        |  15 +++
 .../Exception/CacheableNotFoundHttpException.php   |  15 +++
 .../CacheablePreconditionFailedHttpException.php   |  15 +++
 .../CacheablePreconditionRequiredHttpException.php |  15 +++
 .../CacheableServiceUnavailableHttpException.php   |  15 +++
 .../CacheableTooManyRequestsHttpException.php      |  15 +++
 .../CacheableUnauthorizedHttpException.php         |  15 +++
 .../CacheableUnprocessableEntityHttpException.php  |  15 +++
 .../CacheableUnsupportedMediaTypeHttpException.php |  15 +++
 .../DefaultExceptionHtmlSubscriber.php             |   7 ++
 .../EventSubscriber/DefaultExceptionSubscriber.php |  12 ++-
 .../EventSubscriber/ExceptionJsonSubscriber.php    |  13 ++-
 .../src/Authentication/Provider/BasicAuth.php      |  30 +++++-
 .../src/Plugin/rest/resource/EntityResource.php    |   5 +-
 .../tests/modules/rest_test/rest_test.services.yml |   5 +
 .../RequestPolicy/DenyTestAuthRequests.php         |  26 ++++++
 .../tests/src/Functional/AnonResourceTestTrait.php |   2 +-
 .../src/Functional/BasicAuthResourceTestTrait.php  |   6 +-
 .../src/Functional/CookieResourceTestTrait.php     |  20 +++-
 .../EntityResource/Block/BlockResourceTestBase.php |  15 +++
 .../Comment/CommentResourceTestBase.php            |   9 ++
 .../EntityResource/EntityResourceTestBase.php      |  67 +++++++-------
 .../SearchPage/SearchPageResourceTestBase.php      |   9 ++
 .../EntityResource/User/UserResourceTestBase.php   |  15 ++-
 .../rest/tests/src/Functional/ResourceTestBase.php | 101 +++++++++++++++++++--
 .../EventSubscriber/DefaultExceptionSubscriber.php |  23 ++++-
 .../src/EventSubscriber/AdminRouteSubscriber.php   |  20 +++-
 core/modules/user/src/UserAccessControlHandler.php |   2 +-
 38 files changed, 608 insertions(+), 63 deletions(-)

diff --git a/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php b/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php
index cbac2d1..7d8c23a 100644
--- a/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php
+++ b/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php
@@ -6,6 +6,7 @@
  * Defines an interface for responses that can expose cacheability metadata.
  *
  * @see \Drupal\Core\Cache\CacheableResponseTrait
+ * @see \Drupal\Core\Cache\Exception\CacheableHttpExceptionInterface
  */
 interface CacheableResponseInterface {
 
diff --git a/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php b/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php
index 2940c13..a6da869 100644
--- a/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php
+++ b/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php
@@ -3,9 +3,10 @@
 namespace Drupal\Core\Cache;
 
 /**
- * Provides an implementation of CacheableResponseInterface.
+ * Provides an implementation for cacheable responses/HTTP exceptions.
  *
  * @see \Drupal\Core\Cache\CacheableResponseInterface
+ * @see \Drupal\Core\Cache\Exception\CacheableHttpExceptionInterface
  */
 trait CacheableResponseTrait {
 
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableAccessDeniedHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableAccessDeniedHttpException.php
new file mode 100644
index 0000000..6e38512
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableAccessDeniedHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+
+/**
+ * A cacheable AccessDeniedHttpException.
+ */
+class CacheableAccessDeniedHttpException extends AccessDeniedHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableBadRequestHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableBadRequestHttpException.php
new file mode 100644
index 0000000..5bf0461
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableBadRequestHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * A cacheable BadRequestHttpException.
+ */
+class CacheableBadRequestHttpException extends BadRequestHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableConflictHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableConflictHttpException.php
new file mode 100644
index 0000000..2595ce5
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableConflictHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
+
+/**
+ * A cacheable ConflictHttpException.
+ */
+class CacheableConflictHttpException extends ConflictHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableGoneHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableGoneHttpException.php
new file mode 100644
index 0000000..7949f5d
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableGoneHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\GoneHttpException;
+
+/**
+ * A cacheable GoneHttpException.
+ */
+class CacheableGoneHttpException extends GoneHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableHttpException.php
new file mode 100644
index 0000000..5d9be52
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * A cacheable HttpException.
+ */
+class CacheableHttpException extends HttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableHttpExceptionInterface.php b/core/lib/Drupal/Core/Cache/Exception/CacheableHttpExceptionInterface.php
new file mode 100644
index 0000000..3c9f8e2
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableHttpExceptionInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
+
+/**
+ * Interface for HTTP exceptions that can expose cacheability metadata.
+ *
+ * @see \Drupal\Core\Cache\CacheableResponseTrait
+ * @see \Drupal\Core\Cache\CacheableResponseInterface
+ */
+interface CacheableHttpExceptionInterface extends HttpExceptionInterface {
+
+  /**
+   * Adds a dependency on an object: merges its cacheability metadata.
+   *
+   * For instance, when a response depends on some configuration, an entity, or
+   * an access result, we must make sure their cacheability metadata is present
+   * on the response. This method makes doing that simple.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency
+   *   The dependency. If the object implements CacheableDependencyInterface,
+   *   then its cacheability metadata will be used. Otherwise, the passed in
+   *   object must be assumed to be uncacheable, so max-age 0 is set.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Cache\CacheableMetadata::createFromObject()
+   */
+  public function addCacheableDependency($dependency);
+
+  /**
+   * Returns the cacheability metadata for this response.
+   *
+   * @return \Drupal\Core\Cache\CacheableMetadata
+   */
+  public function getCacheableMetadata();
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableLengthRequiredHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableLengthRequiredHttpException.php
new file mode 100644
index 0000000..cd11879
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableLengthRequiredHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
+
+/**
+ * A cacheable LengthRequiredHttpException.
+ */
+class CacheableLengthRequiredHttpException extends LengthRequiredHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableMethodNotAllowedHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableMethodNotAllowedHttpException.php
new file mode 100644
index 0000000..48669b7
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableMethodNotAllowedHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
+
+/**
+ * A cacheable MethodNotAllowedHttpException.
+ */
+class CacheableMethodNotAllowedHttpException extends MethodNotAllowedHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableNotAcceptableHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableNotAcceptableHttpException.php
new file mode 100644
index 0000000..3d85a7f
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableNotAcceptableHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
+
+/**
+ * A cacheable NotAcceptableHttpException.
+ */
+class CacheableNotAcceptableHttpException extends NotAcceptableHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableNotFoundHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableNotFoundHttpException.php
new file mode 100644
index 0000000..183f191
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableNotFoundHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * A cacheable NotFoundHttpException.
+ */
+class CacheableNotFoundHttpException extends NotFoundHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheablePreconditionFailedHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheablePreconditionFailedHttpException.php
new file mode 100644
index 0000000..9c78d7b
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheablePreconditionFailedHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
+
+/**
+ * A cacheable PreconditionFailedHttpException.
+ */
+class CacheablePreconditionFailedHttpException extends PreconditionFailedHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheablePreconditionRequiredHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheablePreconditionRequiredHttpException.php
new file mode 100644
index 0000000..1d4b089
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheablePreconditionRequiredHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
+
+/**
+ * A cacheable PreconditionRequiredHttpException.
+ */
+class CacheablePreconditionRequiredHttpException extends PreconditionRequiredHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableServiceUnavailableHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableServiceUnavailableHttpException.php
new file mode 100644
index 0000000..23e4d97
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableServiceUnavailableHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
+
+/**
+ * A cacheable ServiceUnavailableHttpException.
+ */
+class CacheableServiceUnavailableHttpException extends ServiceUnavailableHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableTooManyRequestsHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableTooManyRequestsHttpException.php
new file mode 100644
index 0000000..0eb7bcd
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableTooManyRequestsHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
+
+/**
+ * A cacheable TooManyRequestsHttpException.
+ */
+class CacheableTooManyRequestsHttpException extends TooManyRequestsHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableUnauthorizedHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableUnauthorizedHttpException.php
new file mode 100644
index 0000000..2f730cd
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableUnauthorizedHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
+
+/**
+ * A cacheable UnauthorizedHttpException.
+ */
+class CacheableUnauthorizedHttpException extends UnauthorizedHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableUnprocessableEntityHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableUnprocessableEntityHttpException.php
new file mode 100644
index 0000000..e8fdb89
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableUnprocessableEntityHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+
+/**
+ * A cacheable UnprocessableEntityHttpException.
+ */
+class CacheableUnprocessableEntityHttpException extends UnprocessableEntityHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Exception/CacheableUnsupportedMediaTypeHttpException.php b/core/lib/Drupal/Core/Cache/Exception/CacheableUnsupportedMediaTypeHttpException.php
new file mode 100644
index 0000000..f2413c7
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Exception/CacheableUnsupportedMediaTypeHttpException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Cache\Exception;
+
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
+
+/**
+ * A cacheable UnsupportedMediaTypeHttpException.
+ */
+class CacheableUnsupportedMediaTypeHttpException extends UnsupportedMediaTypeHttpException implements CacheableHttpExceptionInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
index 38cb895..43b9f8c 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Cache\Exception\CacheableHttpExceptionInterface;
 use Drupal\Core\Routing\RedirectDestinationInterface;
 use Drupal\Core\Utility\Error;
 use Psr\Log\LoggerInterface;
@@ -158,6 +160,11 @@ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $st
         $response->setStatusCode($status_code);
       }
 
+      // Persist the exception's cacheability metadata, if any.
+      if ($response instanceof CacheableResponseInterface && $exception instanceof CacheableHttpExceptionInterface) {
+        $response->addCacheableDependency($exception->getCacheableMetadata());
+      }
+
       // Persist any special HTTP headers that were set on the exception.
       if ($exception instanceof HttpExceptionInterface) {
         $response->headers->add($exception->getHeaders());
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
index 7b5ecc1..f4651d1 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
@@ -3,6 +3,8 @@
 namespace Drupal\Core\EventSubscriber;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableResponse;
+use Drupal\Core\Cache\Exception\CacheableHttpExceptionInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Utility\Error;
@@ -118,7 +120,15 @@ protected function onHtml(GetResponseForExceptionEvent $event) {
 
     $content = $this->t('The website encountered an unexpected error. Please try again later.');
     $content .= $message ? '</br></br>' . $message : '';
-    $response = new Response($content, 500, ['Content-Type' => 'text/plain']);
+
+    // If the exception is cacheable, generate a cacheable response.
+    if ($exception instanceof CacheableHttpExceptionInterface) {
+      $response = new CacheableResponse($content, 500, ['Content-Type' => 'text/plain']);
+      $response->addCacheableDependency($exception->getCacheableMetadata());
+    }
+    else {
+      $response = new Response($content, 500, ['Content-Type' => 'text/plain']);
+    }
 
     if ($exception instanceof HttpExceptionInterface) {
       $response->setStatusCode($exception->getStatusCode());
diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
index 6ccb743..1c43f3b 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Cache\CacheableJsonResponse;
+use Drupal\Core\Cache\Exception\CacheableHttpExceptionInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
 
@@ -35,7 +37,16 @@ protected static function getPriority() {
   public function on4xx(GetResponseForExceptionEvent $event) {
     /** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception */
     $exception = $event->getException();
-    $response = new JsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
+
+    // If the exception is cacheable, generate a cacheable response.
+    if ($exception instanceof CacheableHttpExceptionInterface) {
+      $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
+      $response->addCacheableDependency($exception->getCacheableMetadata());
+    }
+    else {
+      $response = new JsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
+    }
+
     $event->setResponse($response);
   }
 
diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
index c72e3f0..8b59125 100644
--- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -5,6 +5,9 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Authentication\AuthenticationProviderInterface;
 use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\Exception\CacheableHttpExceptionInterface;
+use Drupal\Core\Cache\Exception\CacheableUnauthorizedHttpException;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Flood\FloodInterface;
@@ -130,7 +133,32 @@ public function challengeException(Request $request, \Exception $previous) {
     $challenge = SafeMarkup::format('Basic realm="@realm"', [
       '@realm' => !empty($site_name) ? $site_name : 'Access restricted',
     ]);
-    return new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
+
+    // A 403 is converted to a 401 here, but it doesn't matter what the
+    // cacheability was of the 403 exception: what matters here is that
+    // authentication credentials are missing, i.e. that this request was made
+    // as the anonymous user.
+    // Therefore, all we must do, is make this response:
+    // 1. vary by whether the current user has the 'anonymous' role or not. This
+    //    works fine because:
+    //    - Page Cache never caches a response whose request has Basic Auth
+    //      credentials thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests.
+    //    - Dynamic Page Cache will cache a different result for when the
+    //      request is unauthenticated (this 401) versus authenticated (some
+    //      other response)
+    // 2. have the 'config:user.role.anonymous' cache tag, because the only
+    //    reason this 401 would no longer be a 401 is if permissions for the
+    //    'anonymous' role change, causing that cache tag to be invalidated.
+    // @see \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
+    // @see \Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber()
+    // @see \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onAllResponds()
+    $exception = new CacheableUnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
+    $exception->addCacheableDependency((new CacheableMetadata())
+      ->setCacheTags(['config:user.role.anonymous'])
+      ->setCacheContexts(['user.roles:anonymous'])
+    );
+
+    return $exception;
   }
 
 }
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 9b1d15f..ea9490a 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Plugin\DependentPluginInterface;
 use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Cache\Exception\CacheableAccessDeniedHttpException;
 use Drupal\Core\Config\Entity\ConfigEntityType;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
@@ -122,7 +123,9 @@ public static function create(ContainerInterface $container, array $configuratio
   public function get(EntityInterface $entity) {
     $entity_access = $entity->access('view', NULL, TRUE);
     if (!$entity_access->isAllowed()) {
-      throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
+      $exception = new CacheableAccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
+      $exception->addCacheableDependency($entity_access);
+      throw $exception;
     }
 
     $response = new ResourceResponse($entity, 200);
diff --git a/core/modules/rest/tests/modules/rest_test/rest_test.services.yml b/core/modules/rest/tests/modules/rest_test/rest_test.services.yml
index ccdbeae..d316cf6 100644
--- a/core/modules/rest/tests/modules/rest_test/rest_test.services.yml
+++ b/core/modules/rest/tests/modules/rest_test/rest_test.services.yml
@@ -7,3 +7,8 @@ services:
     class: Drupal\rest_test\Authentication\Provider\TestAuthGlobal
     tags:
       - { name: authentication_provider, provider_id: 'rest_test_auth_global', global: TRUE }
+  rest_test.page_cache_request_policy.deny_test_auth_requests:
+      class: Drupal\rest_test\PageCache\RequestPolicy\DenyTestAuthRequests
+      public: false
+      tags:
+        - { name: page_cache_request_policy }
diff --git a/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php b/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php
new file mode 100644
index 0000000..8b547a9
--- /dev/null
+++ b/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\rest_test\PageCache\RequestPolicy;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Cache policy for pages requested with REST Test Auth.
+ *
+ * @see \Drupal\rest_test\Authentication\Provider\TestAuth
+ * @see \Drupal\rest_test\Authentication\Provider\TestAuthGlobal
+ * @see \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests
+ */
+class DenyTestAuthRequests implements RequestPolicyInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function check(Request $request) {
+    if ($request->headers->has('REST-test-auth') || $request->headers->has('REST-test-auth-global')) {
+      return self::DENY;
+    }
+  }
+
+}
diff --git a/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php b/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php
index b05ddf2..6527433 100644
--- a/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/AnonResourceTestTrait.php
@@ -24,7 +24,7 @@
   /**
    * {@inheritdoc}
    */
-  protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
+  protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
     throw new \LogicException('When testing for anonymous users, authentication cannot be missing.');
   }
 
diff --git a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
index 6f8c621..9fbdfe0 100644
--- a/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
@@ -31,8 +31,10 @@ protected function getAuthenticationRequestOptions($method) {
   /**
    * {@inheritdoc}
    */
-  protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
-    $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response);
+  protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
+    $expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
+    // @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
+    $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:user.role.anonymous', 'http_response'], ['user.roles:anonymous'], $expected_page_cache_header_value, $expected_page_cache_header_value);
   }
 
   /**
diff --git a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
index 8975c3f..1b763bc 100644
--- a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
@@ -91,11 +91,27 @@ protected function getAuthenticationRequestOptions($method) {
   /**
    * {@inheritdoc}
    */
-  protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
+  protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
     // Requests needing cookie authentication but missing it results in a 403
     // response. The cookie authentication mechanism sets no response message.
+    // Hence, effectively, this is just the 403 response that one gets as the
+    // anonymous user trying to access a certain REST resource.
+    // @see \Drupal\user\Authentication\Provider\Cookie
     // @todo https://www.drupal.org/node/2847623
-    $this->assertResourceErrorResponse(403, FALSE, $response);
+    if ($method === 'GET') {
+      $expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
+      // - \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies
+      //   to cacheable anonymous responses: it updates their cacheability.
+      // - A 403 response to a GET request is cacheable.
+      // Therefore we must update our cacheability expectations accordingly.
+      if (in_array('user.permissions', $expected_cookie_403_cacheability->getCacheContexts(), TRUE)) {
+        $expected_cookie_403_cacheability->addCacheTags(['config:user.role.anonymous']);
+      }
+      $this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', 'MISS');
+    }
+    else {
+      $this->assertResourceErrorResponse(403, FALSE, $response);
+    }
   }
 
   /**
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 d86f9b1..6f0010b 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
@@ -141,4 +141,19 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessCacheability() {
+    return parent::getExpectedUnauthorizedAccessCacheability()
+      // @see \Drupal\block\BlockAccessControlHandler::checkAccess()
+      ->setCacheTags([
+        '4xx-response',
+        'config:block.block.llama',
+        'http_response',
+        static::$auth ? 'user:2' : 'user:0',
+      ])
+      ->setCacheContexts(['user.roles']);
+  }
+
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
index b46eec9..74339dc 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -327,4 +327,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessCacheability() {
+    return parent::getExpectedUnauthorizedAccessCacheability()
+      // @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
+      ->addCacheTags(['comment:1']);
+  }
+
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 81baf9e..3139256 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Url;
@@ -256,6 +257,17 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
   /**
    * {@inheritdoc}
    */
+  protected function getExpectedUnauthorizedAccessCacheability() {
+    return (new CacheableMetadata())
+      ->setCacheTags(static::$auth
+        ? ['4xx-response', 'http_response']
+        : ['4xx-response', 'config:user.role.anonymous', 'http_response'])
+      ->setCacheContexts(['user.permissions']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   protected function getExpectedBcUnauthorizedAccessMessage($method) {
     return "The 'restful " . strtolower($method) . " entity:" . $this->entity->getEntityTypeId() . "' permission is required.";
   }
@@ -353,7 +365,7 @@ public function testGet() {
     // response.
     if (static::$auth) {
       $response = $this->request('GET', $url, $request_options);
-      $this->assertResponseWhenMissingAuthentication($response);
+      $this->assertResponseWhenMissingAuthentication('GET', $response);
     }
 
     $request_options[RequestOptions::HEADERS]['REST-test-auth'] = '1';
@@ -375,7 +387,8 @@ public function testGet() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('GET', $url, $request_options);
-    $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
+    $expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
+    $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', 'MISS');
     $this->assertArrayNotHasKey('Link', $response->getHeaders());
 
 
@@ -385,7 +398,7 @@ public function testGet() {
 
     // 200 for well-formed HEAD request.
     $response = $this->request('HEAD', $url, $request_options);
-    $this->assertResourceResponse(200, '', $response);
+    $this->assertResourceResponse(200, '', $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', 'MISS');
     if (!$this->account) {
       $this->assertSame(['MISS'], $response->getHeader('X-Drupal-Cache'));
     }
@@ -396,17 +409,7 @@ public function testGet() {
 
     // 200 for well-formed GET request. Page Cache hit because of HEAD request.
     $response = $this->request('GET', $url, $request_options);
-    $this->assertResourceResponse(200, FALSE, $response);
-    if (!static::$auth) {
-      $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Cache'));
-    }
-    else {
-      $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
-    }
-    $cache_tags_header_value = $response->getHeader('X-Drupal-Cache-Tags')[0];
-    $this->assertEquals($this->getExpectedCacheTags(), empty($cache_tags_header_value) ? [] : explode(' ', $cache_tags_header_value));
-    $cache_contexts_header_value = $response->getHeader('X-Drupal-Cache-Contexts')[0];
-    $this->assertEquals($this->getExpectedCacheContexts(), empty($cache_contexts_header_value) ? [] : explode(' ', $cache_contexts_header_value));
+    $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'HIT', static::$auth ? 'HIT' : 'MISS');
     // Sort the serialization data first so we can do an identical comparison
     // for the keys with the array order the same (it needs to match with
     // identical comparison).
@@ -464,7 +467,7 @@ public function testGet() {
 
 
       $response = $this->request('GET', $url, $request_options);
-      $this->assertResourceResponse(200, FALSE, $response);
+      $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', 'MISS');
 
 
       // Again do an identical comparison, but this time transform the expected
@@ -496,7 +499,16 @@ public function testGet() {
 
     // 200 for well-formed request.
     $response = $this->request('GET', $url, $request_options);
-    $this->assertResourceResponse(200, FALSE, $response);
+    $expected_cache_tags = $this->getExpectedCacheTags();
+    $expected_cache_contexts = $this->getExpectedCacheContexts();
+    // \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies to
+    // cacheable anonymous responses: it updates their cacheability. Therefore
+    // we must update our cacheability expectations for anonymous responses
+    // accordingly.
+    if (!static::$auth && in_array('user.permissions', $expected_cache_contexts, TRUE)) {
+      $expected_cache_tags = Cache::mergeTags($expected_cache_tags, ['config:user.role.anonymous']);
+    }
+    $this->assertResourceResponse(200, FALSE, $response, $expected_cache_tags, $expected_cache_contexts, static::$auth ? FALSE : 'MISS', 'MISS');
 
 
     $this->resourceConfigStorage->load(static::$resourceConfigId)->disable()->save();
@@ -513,7 +525,7 @@ public function testGet() {
 
     // DX: upon re-enabling a resource, immediate 200.
     $response = $this->request('GET', $url, $request_options);
-    $this->assertResourceResponse(200, FALSE, $response);
+    $this->assertResourceResponse(200, FALSE, $response, $expected_cache_tags, $expected_cache_contexts, static::$auth ? FALSE : 'MISS', 'MISS');
 
 
     $this->resourceConfigStorage->load(static::$resourceConfigId)->delete();
@@ -669,7 +681,7 @@ public function testPost() {
       // DX: forgetting authentication: authentication provider-specific error
       // response.
       $response = $this->request('POST', $url, $request_options);
-      $this->assertResponseWhenMissingAuthentication($response);
+      $this->assertResponseWhenMissingAuthentication('POST', $response);
     }
 
 
@@ -869,7 +881,7 @@ public function testPatch() {
       // DX: forgetting authentication: authentication provider-specific error
       // response.
       $response = $this->request('PATCH', $url, $request_options);
-      $this->assertResponseWhenMissingAuthentication($response);
+      $this->assertResponseWhenMissingAuthentication('PATCH', $response);
     }
 
 
@@ -1024,7 +1036,7 @@ public function testDelete() {
       // DX: forgetting authentication: authentication provider-specific error
       // response.
       $response = $this->request('DELETE', $url, $request_options);
-      $this->assertResponseWhenMissingAuthentication($response);
+      $this->assertResponseWhenMissingAuthentication('DELETE', $response);
     }
 
 
@@ -1046,14 +1058,7 @@ public function testDelete() {
 
     // 204 for well-formed request.
     $response = $this->request('DELETE', $url, $request_options);
-    $this->assertSame(204, $response->getStatusCode());
-    // DELETE responses should not include a Content-Type header. But Apache
-    // sets it to 'text/html' by default. We also cannot detect the presence of
-    // Apache either here in the CLI. For now having this documented here is all
-    // we can do.
-    // $this->assertSame(FALSE, $response->hasHeader('Content-Type'));
-    $this->assertSame('', (string) $response->getBody());
-    $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
+    $this->assertResourceResponse(204, '', $response);
 
 
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
@@ -1072,11 +1077,7 @@ public function testDelete() {
 
     // 204 for well-formed request.
     $response = $this->request('DELETE', $url, $request_options);
-    $this->assertSame(204, $response->getStatusCode());
-    // @todo Uncomment the following line when https://www.drupal.org/node/2821711 is fixed.
-    // $this->assertSame(FALSE, $response->hasHeader('Content-Type'));
-    $this->assertSame('', (string) $response->getBody());
-    $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
+    $this->assertResourceResponse(204, '', $response);
   }
 
   /**
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/SearchPage/SearchPageResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/SearchPage/SearchPageResourceTestBase.php
index e81993f..9b92256 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/SearchPage/SearchPageResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/SearchPage/SearchPageResourceTestBase.php
@@ -99,4 +99,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessCacheability() {
+    return parent::getExpectedUnauthorizedAccessCacheability()
+      // @see \Drupal\search\SearchPageAccessControlHandler::checkAccess()
+      ->addCacheTags(['config:search.page.hinode_search']);
+  }
+
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
index 04fe435..0202c6c 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
@@ -163,7 +163,7 @@ public function testPatchDxForSecuritySensitiveBaseFields() {
 
     // DX: 422 when changing email without providing the password.
     $response = $this->request('PATCH', $url, $request_options);
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response);
+    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
 
 
     $normalization['pass'] = [['existing' => 'wrong']];
@@ -171,7 +171,7 @@ public function testPatchDxForSecuritySensitiveBaseFields() {
 
     // DX: 422 when changing email while providing a wrong password.
     $response = $this->request('PATCH', $url, $request_options);
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response);
+    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
 
 
     $normalization['pass'] = [['existing' => $this->account->passRaw]];
@@ -192,7 +192,7 @@ public function testPatchDxForSecuritySensitiveBaseFields() {
 
     // DX: 422 when changing password without providing the current password.
     $response = $this->request('PATCH', $url, $request_options);
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response);
+    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response, FALSE, FALSE, FALSE, FALSE);
 
 
     $normalization['pass'][0]['existing'] = $this->account->pass_raw;
@@ -237,4 +237,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessCacheability() {
+    return parent::getExpectedUnauthorizedAccessCacheability()
+      // @see \Drupal\user\UserAccessControlHandler::checkAccess()
+      ->addCacheTags(['user:3']);
+  }
+
 }
diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
index c346157..4231d3a 100644
--- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
@@ -204,8 +204,13 @@ protected function refreshTestStateAfterRestConfigChange() {
 
   /**
    * Verifies the error response in case of missing authentication.
+   *
+   * @param string $method
+   *   HTTP method.
+   * @param \Psr\Http\Message\ResponseInterface $response
+   *   The response to assert.
    */
-  abstract protected function assertResponseWhenMissingAuthentication(ResponseInterface $response);
+  abstract protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response);
 
   /**
    * Asserts normalization-specific edge cases.
@@ -251,6 +256,14 @@ protected function refreshTestStateAfterRestConfigChange() {
   abstract protected function getExpectedUnauthorizedAccessMessage($method);
 
   /**
+   * Returns the expected cacheability of an unauthorized access response.
+   *
+   * @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface
+   *   The expected cacheability.
+   */
+  abstract protected function getExpectedUnauthorizedAccessCacheability();
+
+  /**
    * Return the default expected error message if the
    * bc_entity_resource_permissions is true.
    *
@@ -356,17 +369,72 @@ protected function request($method, Url $url, array $request_options) {
    *   The expected response body. FALSE in case this should not be asserted.
    * @param \Psr\Http\Message\ResponseInterface $response
    *   The response to assert.
+   * @param string[]|false $expected_cache_tags
+   *   (optional) The expected cache tags in the X-Drupal-Cache-Tags response
+   *   header, or FALSE if that header should be absent. Defaults to FALSE.
+   * @param string[]|false $expected_cache_contexts
+   *   (optional) The expected cache contexts in the X-Drupal-Cache-Contexts
+   *   response header, or FALSE if that header should be absent. Defaults to
+   *   FALSE.
+   * @param string|false $expected_page_cache_header_value
+   *   (optional) The expected X-Drupal-Cache response header value, or FALSE if
+   *   that header should be absent. Possible strings: 'MISS', 'HIT'. Defaults
+   *   to FALSE.
+   * @param string|false $expected_dynamic_page_cache_header_value
+   *   (optional) The expected X-Drupal-Dynamic-Cache response header value, or
+   *   FALSE if that header should be absent. Possible strings: 'MISS', 'HIT'.
+   *   Defaults to FALSE.
    */
-  protected function assertResourceResponse($expected_status_code, $expected_body, ResponseInterface $response) {
+  protected function assertResourceResponse($expected_status_code, $expected_body, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
     $this->assertSame($expected_status_code, $response->getStatusCode());
-    if ($expected_status_code < 400) {
-      $this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
+    if ($expected_status_code === 204) {
+      // DELETE responses should not include a Content-Type header. But Apache
+      // sets it to 'text/html' by default. We also cannot detect the presence
+      // of Apache either here in the CLI. For now having this documented here
+      // is all we can do.
+      // $this->assertSame(FALSE, $response->hasHeader('Content-Type'));
+      $this->assertSame('', (string) $response->getBody());
     }
     else {
-      $this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
+      if ($expected_status_code < 400) {
+        $this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
+      }
+      else {
+        $this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
+      }
+      if ($expected_body !== FALSE) {
+        $this->assertSame($expected_body, (string) $response->getBody());
+      }
     }
-    if ($expected_body !== FALSE) {
-      $this->assertSame($expected_body, (string) $response->getBody());
+
+    // Expected cache tags: X-Drupal-Cache-Tags header.
+    $this->assertSame($expected_cache_tags !== FALSE, $response->hasHeader('X-Drupal-Cache-Tags'));
+    if (is_array($expected_cache_tags)) {
+      $this->assertSame($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]));
+    }
+
+    // Expected cache contexts: X-Drupal-Cache-Contexts header.
+    $this->assertSame($expected_cache_contexts !== FALSE, $response->hasHeader('X-Drupal-Cache-Contexts'));
+    if (is_array($expected_cache_contexts)) {
+      $this->assertSame($expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
+    }
+
+    // Expected Page Cache header value: X-Drupal-Cache header.
+    if ($expected_page_cache_header_value !== FALSE) {
+      $this->assertTrue($response->hasHeader('X-Drupal-Cache'));
+      $this->assertSame($expected_page_cache_header_value, $response->getHeader('X-Drupal-Cache')[0]);
+    }
+    else {
+      $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
+    }
+
+    // Expected Dynamic Page Cache header value: X-Drupal-Dynamic-Cache header.
+    if ($expected_dynamic_page_cache_header_value !== FALSE) {
+      $this->assertTrue($response->hasHeader('X-Drupal-Dynamic-Cache'));
+      $this->assertSame($expected_dynamic_page_cache_header_value, $response->getHeader('X-Drupal-Dynamic-Cache')[0]);
+    }
+    else {
+      $this->assertFalse($response->hasHeader('X-Drupal-Dynamic-Cache'));
     }
   }
 
@@ -379,10 +447,25 @@ protected function assertResourceResponse($expected_status_code, $expected_body,
    *   The expected error message.
    * @param \Psr\Http\Message\ResponseInterface $response
    *   The error response to assert.
+   * @param string[]|false $expected_cache_tags
+   *   (optional) The expected cache tags in the X-Drupal-Cache-Tags response
+   *   header, or FALSE if that header should be absent. Defaults to FALSE.
+   * @param string[]|false $expected_cache_contexts
+   *   (optional) The expected cache contexts in the X-Drupal-Cache-Contexts
+   *   response header, or FALSE if that header should be absent. Defaults to
+   *   FALSE.
+   * @param string|false $expected_page_cache_header_value
+   *   (optional) The expected X-Drupal-Cache response header value, or FALSE if
+   *   that header should be absent. Possible strings: 'MISS', 'HIT'. Defaults
+   *   to FALSE.
+   * @param string|false $expected_dynamic_page_cache_header_value
+   *   (optional) The expected X-Drupal-Dynamic-Cache response header value, or
+   *   FALSE if that header should be absent. Possible strings: 'MISS', 'HIT'.
+   *   Defaults to FALSE.
    */
-  protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response) {
+  protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
     $expected_body = ($expected_message !== FALSE) ? $this->serializer->encode(['message' => $expected_message], static::$format) : FALSE;
-    $this->assertResourceResponse($expected_status_code, $expected_body, $response);
+    $this->assertResourceResponse($expected_status_code, $expected_body, $response, $expected_cache_tags, $expected_cache_contexts, $expected_page_cache_header_value, $expected_dynamic_page_cache_header_value);
   }
 
   /**
diff --git a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
index a1e7bad..735e80e 100644
--- a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
@@ -2,9 +2,12 @@
 
 namespace Drupal\serialization\EventSubscriber;
 
+use Drupal\Core\Cache\CacheableResponse;
+use Drupal\Core\Cache\Exception\CacheableHttpExceptionInterface;
 use Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 use Symfony\Component\Serializer\SerializerInterface;
 
 /**
@@ -51,8 +54,12 @@ protected function getHandledFormats() {
    */
   protected static function getPriority() {
     // This will fire after the most common HTML handler, since HTML requests
-    // are still more common than HTTP requests.
-    return -75;
+    // are still more common than HTTP requests. But it has a lower priority
+    // than \Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on4xx(), so
+    // that this also handles the 'json' format. Then all serialization formats
+    // (::getHandledFormats()) are handled by this exception subscriber, which
+    // results in better consistency.
+    return -70;
   }
 
   /**
@@ -67,14 +74,22 @@ public function on4xx(GetResponseForExceptionEvent $event) {
     $request = $event->getRequest();
 
     $format = $request->getRequestFormat();
-    $content = ['message' => $event->getException()->getMessage()];
+    $content = ['message' => $exception->getMessage()];
     $encoded_content = $this->serializer->serialize($content, $format);
     $headers = $exception->getHeaders();
 
     // Add the MIME type from the request to send back in the header.
     $headers['Content-Type'] = $request->getMimeType($format);
 
-    $response = new Response($encoded_content, $exception->getStatusCode(), $headers);
+    // If the exception is cacheable, generate a cacheable response.
+    if ($exception instanceof CacheableHttpExceptionInterface) {
+      $response = new CacheableResponse($encoded_content, $exception->getStatusCode(), $headers);
+      $response->addCacheableDependency($exception->getCacheableMetadata());
+    }
+    else {
+      $response = new Response($encoded_content, $exception->getStatusCode(), $headers);
+    }
+
     $event->setResponse($response);
   }
 
diff --git a/core/modules/system/src/EventSubscriber/AdminRouteSubscriber.php b/core/modules/system/src/EventSubscriber/AdminRouteSubscriber.php
index b83fe7e..ac2a47f 100644
--- a/core/modules/system/src/EventSubscriber/AdminRouteSubscriber.php
+++ b/core/modules/system/src/EventSubscriber/AdminRouteSubscriber.php
@@ -4,10 +4,11 @@
 
 use Drupal\Core\Routing\RouteSubscriberBase;
 use Drupal\Core\Routing\RoutingEvents;
+use Symfony\Component\Routing\Route;
 use Symfony\Component\Routing\RouteCollection;
 
 /**
- * Adds the _admin_route option to each admin route.
+ * Adds the _admin_route option to each admin HTML route.
  */
 class AdminRouteSubscriber extends RouteSubscriberBase {
 
@@ -15,14 +16,27 @@ class AdminRouteSubscriber extends RouteSubscriberBase {
    * {@inheritdoc}
    */
   protected function alterRoutes(RouteCollection $collection) {
-    foreach ($collection->all() as $route) {
-      if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route')) {
+    foreach ($collection->all() as $name => $route) {
+      if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route') && static::isHtmlRoute($route)) {
         $route->setOption('_admin_route', TRUE);
       }
     }
   }
 
   /**
+   * Whether the given route is a HTML route.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to inspect.
+   *
+   * @return bool
+   *   Whether the given route is a HTML route.
+   */
+  protected static function isHtmlRoute(Route $route) {
+    return !$route->hasRequirement('_format') || in_array('html', explode('|', $route->getRequirement('_format')), TRUE);
+  }
+
+  /**
    * {@inheritdoc}
    */
   public static function getSubscribedEvents() {
diff --git a/core/modules/user/src/UserAccessControlHandler.php b/core/modules/user/src/UserAccessControlHandler.php
index 712b32a..8ff01d1 100644
--- a/core/modules/user/src/UserAccessControlHandler.php
+++ b/core/modules/user/src/UserAccessControlHandler.php
@@ -58,7 +58,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
           return AccessResult::allowed()->cachePerUser();
         }
         else {
-          return AccessResultNeutral::neutral("The 'access user profiles' permission is required and the user must be active.");
+          return AccessResultNeutral::neutral("The 'access user profiles' permission is required and the user must be active.")->cachePerPermissions()->addCacheableDependency($entity);
         }
         break;
 
