diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 2c6dbc1..d4e203d 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -527,10 +527,8 @@ protected function invalidateTagsOnSave($update) {
     // listing's filtering requirements. A newly created entity may start to
     // appear in listings because it did not exist before.)
     $tags = $this->getEntityType()->getListCacheTags();
-    if ($this->hasLinkTemplate('canonical')) {
-      // Creating or updating an entity may change a cached 403 or 404 response.
-      $tags = Cache::mergeTags($tags, ['4xx-response']);
-    }
+    // Creating or updating an entity may change a cached 403 or 404 response.
+    $tags = Cache::mergeTags($tags, ['4xx-response']);
     if ($update) {
       // An existing entity was updated, also invalidate its unique cache tag.
       $tags = Cache::mergeTags($tags, $this->getCacheTagsToInvalidate());
diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
index 4f44409..02fc0bc 100644
--- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Session\AccountProxyInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -125,6 +126,29 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
   }
 
   /**
+   * Respond with a challenge on a 403 response if appropriate.
+   *
+   * On a 403 (access denied), if there are no credentials on the request, some
+   * authentication methods (e.g. basic auth) require that a challenge is sent
+   * to the client.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The response event.
+   */
+  public function onForbiddenResponse(FilterResponseEvent $event) {
+    if (isset($this->challengeProvider) && $event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
+      $request = $event->getRequest();
+      $response = $event->getResponse();
+      if ($response->getStatusCode() === 403 && !$this->authenticationProvider->applies($request) && (!isset($this->filter) || $this->filter->appliesToRoutedRequest($request, FALSE))) {
+        $challenge_exception = $this->challengeProvider->challengeException($request, new AccessDeniedHttpException());
+        if ($challenge_exception) {
+          throw $challenge_exception;
+        }
+      }
+    }
+  }
+
+  /**
    * {@inheritdoc}
    */
   public static function getSubscribedEvents() {
@@ -136,6 +160,7 @@ public static function getSubscribedEvents() {
 
     // Access check must be performed after routing.
     $events[KernelEvents::REQUEST][] = ['onKernelRequestFilterProvider', 31];
+    $events[KernelEvents::RESPONSE][] = ['onForbiddenResponse', 75];
     $events[KernelEvents::EXCEPTION][] = ['onExceptionSendChallenge', 75];
     return $events;
   }
diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
index 01f3620..70621a5 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\Core\EventSubscriber;
 
-use Symfony\Component\HttpFoundation\JsonResponse;
+use Drupal\Core\Cache\CacheableJsonResponse;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
 
@@ -34,7 +34,7 @@ protected static function getPriority() {
    *   The event to process.
    */
   public function on403(GetResponseForExceptionEvent $event) {
-    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN);
+    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN);
     $event->setResponse($response);
   }
 
@@ -45,7 +45,7 @@ public function on403(GetResponseForExceptionEvent $event) {
    *   The event to process.
    */
   public function on404(GetResponseForExceptionEvent $event) {
-    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND);
+    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND);
     $event->setResponse($response);
   }
 
@@ -56,7 +56,7 @@ public function on404(GetResponseForExceptionEvent $event) {
    *   The event to process.
    */
   public function on405(GetResponseForExceptionEvent $event) {
-    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED);
+    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED);
     $event->setResponse($response);
   }
 
@@ -67,7 +67,7 @@ public function on405(GetResponseForExceptionEvent $event) {
    *   The event to process.
    */
   public function on406(GetResponseForExceptionEvent $event) {
-    $response = new JsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE);
+    $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE);
     $event->setResponse($response);
   }
 
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index dd2c6c5..a2cdda2 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -42,7 +42,10 @@ class EntityResource extends ResourceBase {
   public function get(EntityInterface $entity) {
     $entity_access = $entity->access('view', NULL, TRUE);
     if (!$entity_access->isAllowed()) {
-      throw new AccessDeniedHttpException();
+      $response = new ResourceResponse(NULL, 403);
+      $response->addCacheableDependency($entity);
+      $response->addCacheableDependency($entity_access);
+      return $response;
     }
 
     $response = new ResourceResponse($entity, 200);
@@ -77,8 +80,11 @@ public function post(EntityInterface $entity = NULL) {
       throw new BadRequestHttpException('No entity content received.');
     }
 
-    if (!$entity->access('create')) {
-      throw new AccessDeniedHttpException();
+    $entity_access = $entity->access('create', NULL, TRUE);
+    if (!$entity_access->isAllowed()) {
+      $response = new ResourceResponse(NULL, 403);
+      $response->addCacheableDependency($entity_access);
+      return $response;
     }
     $definition = $this->getPluginDefinition();
     // Verify that the deserialized entity is of the type that we expect to
@@ -96,8 +102,14 @@ public function post(EntityInterface $entity = NULL) {
     // submitted by the user. Field access makes no difference between 'create'
     // and 'update', so the 'edit' operation is used here.
     foreach ($entity->_restSubmittedFields as $key => $field_name) {
-      if (!$entity->get($field_name)->access('edit')) {
-        throw new AccessDeniedHttpException("Access denied on creating field '$field_name'");
+      $field_access = $entity->get($field_name)->access('edit', NULL, TRUE);
+      if (!$field_access->isAllowed()) {
+        $response = new ResourceResponse([
+          'message' => "Access denied on creating field '$field_name'"
+        ], 403);
+        $response->addCacheableDependency($entity_access);
+        $response->addCacheableDependency($field_access);
+        return $response;
       }
     }
 
@@ -141,8 +153,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
     if ($entity->getEntityTypeId() != $definition['entity_type']) {
       throw new BadRequestHttpException('Invalid entity type');
     }
-    if (!$original_entity->access('update')) {
-      throw new AccessDeniedHttpException();
+    $original_entity_access = $original_entity->access('update', NULL, TRUE);
+    if (!$original_entity_access->isAllowed()) {
+      $response = new ResourceResponse(NULL, 403);
+      $response->addCacheableDependency($original_entity);
+      $response->addCacheableDependency($original_entity_access);
+      return $response;
     }
 
     // Overwrite the received properties.
@@ -166,8 +182,15 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
         }
       }
 
-      if (!$original_entity->get($field_name)->access('edit')) {
-        throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+      $field_access = $original_entity->get($field_name)->access('edit', NULL, TRUE);
+      if (!$field_access->isAllowed()) {
+        $response = new ResourceResponse([
+          'message' => "Access denied on updating field '$field_name'."
+        ], 403);
+        $response->addCacheableDependency($original_entity);
+        $response->addCacheableDependency($original_entity_access);
+        $response->addCacheableDependency($field_access);
+        return $response;
       }
       $original_entity->set($field_name, $field->getValue());
     }
@@ -198,8 +221,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
   public function delete(EntityInterface $entity) {
-    if (!$entity->access('delete')) {
-      throw new AccessDeniedHttpException();
+    $entity_access = $entity->access('delete', NULL, TRUE);
+    if (!$entity_access->isAllowed()) {
+      $response = new ResourceResponse(NULL, 403);
+      $response->addCacheableDependency($entity);
+      $response->addCacheableDependency($entity_access);
+      return $response;
     }
     try {
       $entity->delete();
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index 3fd70c4..4b26944 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -2,12 +2,14 @@
 
 namespace Drupal\rest;
 
+use Drupal\Core\Cache\CacheableResponse;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareTrait;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
@@ -74,9 +76,10 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
           $unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method));
         }
         catch (UnexpectedValueException $e) {
-          $error['error'] = $e->getMessage();
-          $content = $serializer->serialize($error, $format);
-          return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
+          // Set the request format so the Exception is returned in the proper
+          // format.
+          $request->setRequestFormat($format);
+          throw new BadRequestHttpException($e->getMessage(), $e);
         }
       }
       else {
@@ -100,17 +103,7 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
     // parsing it out of the Accept headers again, we can simply retrieve the
     // format requirement. If there is no format associated, just pick JSON.
     $format = $route_match->getRouteObject()->getRequirement('_format') ?: 'json';
-    try {
-      $response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
-    }
-    catch (HttpException $e) {
-      $error['error'] = $e->getMessage();
-      $content = $serializer->serialize($error, $format);
-      // Add the default content type, but only if the headers from the
-      // exception have not specified it already.
-      $headers = $e->getHeaders() + array('Content-Type' => $request->getMimeType($format));
-      return new Response($content, $e->getStatusCode(), $headers);
-    }
+    $response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
 
     if ($response instanceof ResourceResponse) {
       $data = $response->getResponseData();
diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php
index 7f0ed81..a2580fd 100644
--- a/core/modules/rest/src/Tests/NodeTest.php
+++ b/core/modules/rest/src/Tests/NodeTest.php
@@ -175,7 +175,7 @@ public function testInvalidBundle() {
 
     // Make sure the response is "Bad Request".
     $this->assertResponse(400);
-    $this->assertResponseBody('{"error":"\"bad_bundle_name\" is not a valid bundle type for denormalization."}');
+    $this->assertResponseBody('{"message":"\\u0022bad_bundle_name\\u0022 is not a valid bundle type for denormalization."}');
   }
 
   /**
@@ -192,7 +192,7 @@ public function testMissingBundle() {
 
     // Make sure the response is "Bad Request".
     $this->assertResponse(400);
-    $this->assertResponseBody('{"error":"A string must be provided as a bundle value."}');
+    $this->assertResponseBody('{"message":"A string must be provided as a bundle value."}');
   }
 
 }
diff --git a/core/modules/rest/src/Tests/UpdateTest.php b/core/modules/rest/src/Tests/UpdateTest.php
index 98df739..778e1b1 100644
--- a/core/modules/rest/src/Tests/UpdateTest.php
+++ b/core/modules/rest/src/Tests/UpdateTest.php
@@ -340,7 +340,7 @@ protected function patchEntity(EntityInterface $entity, array $read_only_fields,
 
       $this->httpRequest($url, 'PATCH', $serialized, $mime_type);
       $this->assertResponse(403);
-      $this->assertResponseBody('{"error":"Access denied on updating field \'' . $field . '\'."}');
+      $this->assertResponseBody('{"message":"Access denied on updating field \'' . $field . '\'."}');
 
       if ($format === 'hal_json') {
         // We've just tried with this read-only field, now unset it.
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php
index f150e4fe..407e75d 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php
@@ -392,11 +392,13 @@ public function testPostSave() {
     $this->cacheTagsInvalidator->expects($this->at(0))
       ->method('invalidateTags')
       ->with(array(
+        '4xx-response',
         $this->entityTypeId . '_list', // List cache tag.
       ));
     $this->cacheTagsInvalidator->expects($this->at(1))
       ->method('invalidateTags')
       ->with(array(
+        '4xx-response',
         $this->entityTypeId . ':' . $this->values['id'], // Own cache tag.
         $this->entityTypeId . '_list', // List cache tag.
       ));
