diff --git a/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php b/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..19cb0f2b7664705dcc8707f1ee9c4f19be57149f
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Trait for \Drupal\Core\Cache\CacheableDependencyInterface.
+ */
+trait CacheableDependencyTrait {
+
+  /**
+   * Cache contexts.
+   *
+   * @var string[]
+   */
+  protected $cacheContexts = [];
+
+  /**
+   * Cache tags.
+   *
+   * @var string[]
+   */
+  protected $cacheTags = [];
+
+  /**
+   * Cache max-age.
+   *
+   * @var int
+   */
+  protected $cacheMaxAge = Cache::PERMANENT;
+
+  /**
+   * Sets cacheability; useful for value object constructors.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheability
+   *   The cacheability to set.
+   *
+   * @return $this
+   */
+  protected function setCacheability(CacheableDependencyInterface $cacheability) {
+    $this->cacheContexts = $cacheability->getCacheContexts();
+    $this->cacheTags = $cacheability->getCacheTags();
+    $this->cacheMaxAge = $cacheability->getCacheMaxAge();
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return $this->cacheTags;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->cacheContexts;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->cacheMaxAge;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php b/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php
index 21b61b220a9bd826c709d92bbee56fbf0f59d358..fcbc11f8d1ecb83d58d2d54e6071a4a956a3c65e 100644
--- a/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php
+++ b/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php
@@ -7,47 +7,7 @@
  */
 trait RefinableCacheableDependencyTrait {
 
-  /**
-   * Cache contexts.
-   *
-   * @var string[]
-   */
-  protected $cacheContexts = [];
-
-  /**
-   * Cache tags.
-   *
-   * @var string[]
-   */
-  protected $cacheTags = [];
-
-  /**
-   * Cache max-age.
-   *
-   * @var int
-   */
-  protected $cacheMaxAge = Cache::PERMANENT;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheTags() {
-    return $this->cacheTags;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheContexts() {
-    return $this->cacheContexts;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheMaxAge() {
-    return $this->cacheMaxAge;
-  }
+  use CacheableDependencyTrait;
 
   /**
    * {@inheritdoc}
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
index f168a02caddf12c99e55bbd4836a334a459f981e..8812583b66c855c1a3fa34b638e4b32fb02f19f5 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\Routing\RedirectDestinationInterface;
 use Drupal\Core\Utility\Error;
 use Psr\Log\LoggerInterface;
@@ -170,6 +171,13 @@ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $st
         $response->setStatusCode($status_code);
       }
 
+      // Persist the exception's cacheability metadata, if any. If the exception
+      // itself isn't cacheable, then this will make the response uncacheable:
+      // max-age=0 will be set.
+      if ($response instanceof CacheableResponseInterface) {
+        $response->addCacheableDependency($exception);
+      }
+
       // 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/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
index 6ccb743356de49dc3c874a7e1ca707ae313a4f6b..239e5821160fbed84ca38c0ea17db921a186b7fa 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\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableJsonResponse;
 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 CacheableDependencyInterface) {
+      $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
+      $response->addCacheableDependency($exception);
+    }
+    else {
+      $response = new JsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
+    }
+
     $event->setResponse($response);
   }
 
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..e0fd5cb671e5b0e1ff62a86b8dadff806841865d
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+
+/**
+ * A cacheable AccessDeniedHttpException.
+ */
+class CacheableAccessDeniedHttpException extends AccessDeniedHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..97d432ae04495b08c0d9195554463209d4ebd8de
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * A cacheable BadRequestHttpException.
+ */
+class CacheableBadRequestHttpException extends BadRequestHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..ca804fb3eb1718ba2f5779860e6389580006ec27
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
+
+/**
+ * A cacheable ConflictHttpException.
+ */
+class CacheableConflictHttpException extends ConflictHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..4568c91d7282b4c605aacc7e7ab3ed7175472be2
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\GoneHttpException;
+
+/**
+ * A cacheable GoneHttpException.
+ */
+class CacheableGoneHttpException extends GoneHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..76f529eea011efad6cd5115ca217e7b241c474f4
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * A cacheable HttpException.
+ */
+class CacheableHttpException extends HttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $statusCode = 0, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($statusCode, $message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..a75f80a6ca9138b8371f5ff6ad814db8ef94560f
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
+
+/**
+ * A cacheable LengthRequiredHttpException.
+ */
+class CacheableLengthRequiredHttpException extends LengthRequiredHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..d9919b1e1e56bce5b11f1d42c85acae9f7fa85d4
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
+
+/**
+ * A cacheable MethodNotAllowedHttpException.
+ */
+class CacheableMethodNotAllowedHttpException extends MethodNotAllowedHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, array $allow, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($allow, $message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..94bf1c2a6c99cd189815e01660f43fa2297af0c8
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
+
+/**
+ * A cacheable NotAcceptableHttpException.
+ */
+class CacheableNotAcceptableHttpException extends NotAcceptableHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..9e5e136cd131a593d668541e7925af735fd4efe2
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * A cacheable NotFoundHttpException.
+ */
+class CacheableNotFoundHttpException extends NotFoundHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..7921d3efc668be02419d89e5ee7cb1e2215521a9
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
+
+/**
+ * A cacheable PreconditionFailedHttpException.
+ */
+class CacheablePreconditionFailedHttpException extends PreconditionFailedHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..d66b255ad8e762bbfbe3a00f71d7f59194895164
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
+
+/**
+ * A cacheable PreconditionRequiredHttpException.
+ */
+class CacheablePreconditionRequiredHttpException extends PreconditionRequiredHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..313b9ae58d6e65a476f221250133aec050311a83
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
+
+/**
+ * A cacheable ServiceUnavailableHttpException.
+ */
+class CacheableServiceUnavailableHttpException extends ServiceUnavailableHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $retryAfter = NULL, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($retryAfter, $message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..e709c0b504fc31b7882bd29b04adf375e29349a3
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
+
+/**
+ * A cacheable TooManyRequestsHttpException.
+ */
+class CacheableTooManyRequestsHttpException extends TooManyRequestsHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $retryAfter = NULL, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($retryAfter, $message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..35dbd72e37d190b0963d436243e28f48d12ccfd2
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
+
+/**
+ * A cacheable UnauthorizedHttpException.
+ */
+class CacheableUnauthorizedHttpException extends UnauthorizedHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $challenge, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($challenge, $message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..655c67a071a42d4ca9dc5eab28717e505fc1fae3
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+
+/**
+ * A cacheable UnprocessableEntityHttpException.
+ */
+class CacheableUnprocessableEntityHttpException extends UnprocessableEntityHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php
new file mode 100644
index 0000000000000000000000000000000000000000..c6f6023daf7197ac096859261a01ace5b55fa6c5
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
+
+/**
+ * A cacheable UnsupportedMediaTypeHttpException.
+ */
+class CacheableUnsupportedMediaTypeHttpException extends UnsupportedMediaTypeHttpException implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0)  {
+    $this->setCacheability($cacheability);
+    parent::__construct($message, $previous, $code);
+  }
+
+}
diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
index c72e3f0c0168e24e49d9a694257d079d85910d09..01034f4a8c44d0406cf2900c8dd13868c70d5cda 100644
--- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -5,12 +5,13 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Authentication\AuthenticationProviderInterface;
 use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Flood\FloodInterface;
+use Drupal\Core\Http\Exception\CacheableUnauthorizedHttpException;
 use Drupal\user\UserAuthInterface;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
 
 /**
  * HTTP Basic authentication provider.
@@ -126,11 +127,35 @@ public function authenticate(Request $request) {
    * {@inheritdoc}
    */
   public function challengeException(Request $request, \Exception $previous) {
-    $site_name = $this->configFactory->get('system.site')->get('name');
+    $site_config = $this->configFactory->get('system.site');
+    $site_name = $site_config->get('name');
     $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:
+    //    - Thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests,
+    //      Page Cache never caches a response whose request has Basic Auth
+    //      credentials.
+    //    - 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()
+    $cacheability = CacheableMetadata::createFromObject($site_config)
+      ->addCacheTags(['config:user.role.anonymous'])
+      ->addCacheContexts(['user.roles:anonymous']);
+    return new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous);
   }
 
 }
diff --git a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
index b197a7631a190f5c1ccdb57b61dc5e35ac5778cb..f844cb930531f6ff4d6edd75c7550450b633662c 100644
--- a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
+++ b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
@@ -7,6 +7,7 @@
 use Drupal\Tests\basic_auth\Traits\BasicAuthTestTrait;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\user\Entity\Role;
 
 /**
  * Tests for BasicAuth authentication provider.
@@ -180,6 +181,47 @@ public function testUnauthorizedErrorMessage() {
     $this->assertText('Access denied', "A user friendly access denied message is displayed");
   }
 
+  /**
+   * Tests the cacheability of Basic Auth's 401 response.
+   *
+   * @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
+   */
+  public function testCacheabilityOf401Response() {
+    $session = $this->getSession();
+    $url = Url::fromRoute('router_test.11');
+
+    $assert_response_cacheability = function ($expected_page_cache_header_value, $expected_dynamic_page_cache_header_value) use ($session, $url) {
+      $this->drupalGet($url);
+      $this->assertSession()->statusCodeEquals(401);
+      $this->assertSame($expected_page_cache_header_value, $session->getResponseHeader('X-Drupal-Cache'));
+      $this->assertSame($expected_dynamic_page_cache_header_value, $session->getResponseHeader('X-Drupal-Dynamic-Cache'));
+    };
+
+    // 1. First request: cold caches, both Page Cache and Dynamic Page Cache are
+    // now primed.
+    $assert_response_cacheability('MISS', 'MISS');
+    // 2. Second request: Page Cache HIT, we don't even hit Dynamic Page Cache.
+    // This is going to keep happening.
+    $assert_response_cacheability('HIT', 'MISS');
+    // 3. Third request: after clearing Page Cache, we now see that Dynamic Page
+    // Cache is a HIT too.
+    $this->container->get('cache.page')->deleteAll();
+    $assert_response_cacheability('MISS', 'HIT');
+    // 4. Fourth request: warm caches.
+    $assert_response_cacheability('HIT', 'HIT');
+
+    // If the permissions of the 'anonymous' role change, it may no longer be
+    // necessary to be authenticated to access this route. Therefore the cached
+    // 401 responses should be invalidated.
+    $this->grantPermissions(Role::load(Role::ANONYMOUS_ID), [$this->randomMachineName()]);
+    $assert_response_cacheability('MISS', 'MISS');
+    $assert_response_cacheability('HIT', 'MISS');
+    // Idem for when the 'system.site' config changes.
+    $this->config('system.site')->save();
+    $assert_response_cacheability('MISS', 'MISS');
+    $assert_response_cacheability('HIT', 'MISS');
+  }
+
   /**
    * Tests if the controller is called before authentication.
    *
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 ccdbeae7385c1d677046dbc88e4a448d6fe57a58..d316cf6072b8d0169aee2c570dc035ff9e33b05a 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 0000000000000000000000000000000000000000..17be647dfad34485637dcdb2cbdec1f0374dc609
--- /dev/null
+++ b/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php
@@ -0,0 +1,31 @@
+<?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.
+ *
+ * This policy disallows caching of requests that use the REST Test Auth
+ * authentication provider for security reasons (just like basic_auth).
+ * Otherwise responses for authenticated requests can get into the page cache
+ * and could be delivered to unprivileged users.
+ *
+ * @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/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 6faf028c2b38a7990f82ee50a5d4124b797013e6..418ddfbb4a7902efaa69b6219150bfd94d2b6072 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -414,14 +414,20 @@ public function testGet() {
           ':pattern' => '%[route]=rest.%',
         ])
         ->fetchAllAssoc('cid');
-      $this->assertCount(2, $cache_items);
+      $this->assertTrue(count($cache_items) >= 2);
       $found_cache_redirect = FALSE;
-      $found_cached_response = FALSE;
+      $found_cached_200_response = FALSE;
+      $other_cached_responses_are_4xx = TRUE;
       foreach ($cache_items as $cid => $cache_item) {
         $cached_data = unserialize($cache_item->data);
         if (!isset($cached_data['#cache_redirect'])) {
-          $found_cached_response = TRUE;
           $cached_response = $cached_data['#response'];
+          if ($cached_response->getStatusCode() === 200) {
+            $found_cached_200_response = TRUE;
+          }
+          elseif (!$cached_response->isClientError()) {
+            $other_cached_responses_are_4xx = FALSE;
+          }
           $this->assertNotInstanceOf(ResourceResponseInterface::class, $cached_response);
           $this->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
         }
@@ -430,7 +436,8 @@ public function testGet() {
         }
       }
       $this->assertTrue($found_cache_redirect);
-      $this->assertTrue($found_cached_response);
+      $this->assertTrue($found_cached_200_response);
+      $this->assertTrue($other_cached_responses_are_4xx);
     }
     $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));
diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php
index 86c6d86bfd5f43ecd66264b44d2faa7013417f43..8f339a839dcdfa283050249f7fc05c69c2536053 100644
--- a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php
+++ b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php
@@ -2,11 +2,15 @@
 
 namespace Drupal\Tests\Core\EventSubscriber;
 
+use Drupal\Core\Cache\CacheableJsonResponse;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\EventSubscriber\ExceptionJsonSubscriber;
+use Drupal\Core\Http\Exception\CacheableMethodNotAllowedHttpException;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
 
@@ -18,21 +22,34 @@ class ExceptionJsonSubscriberTest extends UnitTestCase {
 
   /**
    * @covers ::on4xx
+   * @dataProvider providerTestOn4xx
    */
-  public function testOn4xx() {
+  public function testOn4xx(HttpExceptionInterface $exception, $expected_response_class) {
     $kernel = $this->prophesize(HttpKernelInterface::class);
     $request = Request::create('/test');
-    $e = new MethodNotAllowedHttpException(['POST', 'PUT'], 'test message');
-    $event = new GetResponseForExceptionEvent($kernel->reveal(), $request, 'GET', $e);
+    $event = new GetResponseForExceptionEvent($kernel->reveal(), $request, 'GET', $exception);
     $subscriber = new ExceptionJsonSubscriber();
     $subscriber->on4xx($event);
     $response = $event->getResponse();
 
-    $this->assertInstanceOf(JsonResponse::class, $response);
+    $this->assertInstanceOf($expected_response_class, $response);
     $this->assertEquals('{"message":"test message"}', $response->getContent());
     $this->assertEquals(405, $response->getStatusCode());
     $this->assertEquals('POST, PUT', $response->headers->get('Allow'));
     $this->assertEquals('application/json', $response->headers->get('Content-Type'));
   }
 
+  public function providerTestOn4xx() {
+    return [
+      'uncacheable exception' => [
+        new MethodNotAllowedHttpException(['POST', 'PUT'], 'test message'),
+        JsonResponse::class
+      ],
+      'cacheable exception' => [
+        new CacheableMethodNotAllowedHttpException((new CacheableMetadata())->setCacheContexts(['route']), ['POST', 'PUT'], 'test message'),
+        CacheableJsonResponse::class
+      ],
+    ];
+  }
+
 }