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/dblog/src/Tests/Rest/DbLogResourceTest.php b/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php
index 1dd1f11..fc17ae9 100644
--- a/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php
+++ b/core/modules/dblog/src/Tests/Rest/DbLogResourceTest.php
@@ -53,13 +53,13 @@ public function testWatchdog() {
     $response = $this->httpRequest(Url::fromRoute('rest.dblog.GET.' . $this->defaultFormat, ['id' => 9999, '_format' => $this->defaultFormat]), 'GET');
     $this->assertResponse(404);
     $decoded = Json::decode($response);
-    $this->assertEqual($decoded['error'], 'Log entry with ID 9999 was not found', 'Response message is correct.');
+    $this->assertEqual($decoded['message'], 'Log entry with ID 9999 was not found', 'Response message is correct.');
 
     // Make a bad request (a true malformed request would never be a route match).
     $response = $this->httpRequest(Url::fromRoute('rest.dblog.GET.' . $this->defaultFormat, ['id' => 0, '_format' => $this->defaultFormat]), 'GET');
     $this->assertResponse(400);
     $decoded = Json::decode($response);
-    $this->assertEqual($decoded['error'], 'No log entry ID was provided', 'Response message is correct.');
+    $this->assertEqual($decoded['message'], 'A fatal error occurred: No log entry ID was provided', 'Response message is correct.');
   }
 
 }
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index dd2c6c5..f4c9a28 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\rest\Plugin\ResourceBase;
 use Drupal\rest\ResourceResponse;
+use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -42,7 +43,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);
@@ -66,19 +70,24 @@ public function get(EntityInterface $entity) {
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
    *
    * @return \Drupal\rest\ResourceResponse
    *   The HTTP response object.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
-  public function post(EntityInterface $entity = NULL) {
+  public function post(EntityInterface $entity = NULL, Request $request) {
     if ($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,11 +105,19 @@ 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;
       }
     }
 
+    // Set the reqeust format for the repsonse exceptions.
+    $request->setRequestFormat($request->getContentType());
     // Validate the received data before saving.
     $this->validate($entity);
     try {
@@ -127,13 +144,15 @@ public function post(EntityInterface $entity = NULL) {
    *   The original entity object.
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
    *
    * @return \Drupal\rest\ResourceResponse
    *   The HTTP response object.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
-  public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL) {
+  public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL, Request $request) {
     if ($entity == NULL) {
       throw new BadRequestHttpException('No entity content received.');
     }
@@ -141,8 +160,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,12 +189,21 @@ 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());
     }
 
+    // Set the reqeust format for the repsonse exceptions.
+    $request->setRequestFormat($request->getContentType());
     // Validate the received data before saving.
     $this->validate($original_entity);
     try {
@@ -198,8 +230,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/CreateTest.php b/core/modules/rest/src/Tests/CreateTest.php
index 035480b..baff514 100644
--- a/core/modules/rest/src/Tests/CreateTest.php
+++ b/core/modules/rest/src/Tests/CreateTest.php
@@ -442,8 +442,7 @@ public function assertCreateEntityInvalidSerialized(EntityInterface $entity, $en
     $this->assertResponse(422);
 
     // Verify that the text of the response is correct.
-    $error = Json::decode($response);
-    $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
+    $this->assertEqual($response, "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
   }
 
   /**
diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php
index 7f0ed81..92ca4a8 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":"A fatal error occurred: \\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 fatal error occurred: 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..6428b40 100644
--- a/core/modules/rest/src/Tests/UpdateTest.php
+++ b/core/modules/rest/src/Tests/UpdateTest.php
@@ -159,8 +159,7 @@ public function testPatchUpdate() {
     $invalid_serialized = $serializer->serialize($entity, $this->defaultFormat, $context);
     $response = $this->httpRequest($entity->urlInfo(), 'PATCH', $invalid_serialized, $this->defaultMimeType);
     $this->assertResponse(422);
-    $error = Json::decode($response);
-    $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
+    $this->assertEqual($response, "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
 
     // Try to update an entity without proper permissions.
     $this->drupalLogout();
@@ -199,16 +198,14 @@ public function testUpdateUser() {
     $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
     $response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(422);
-    $error = Json::decode($response);
-    $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
+    $this->assertEqual($response, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
 
     // Try and send the new email with a password.
     $normalized['pass'][0]['existing'] = 'wrong';
     $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
     $response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(422);
-    $error = Json::decode($response);
-    $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
+    $this->assertEqual($response, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
 
     // Try again with the password.
     $normalized['pass'][0]['existing'] = $account->pass_raw;
@@ -223,8 +220,7 @@ public function testUpdateUser() {
     $serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
     $response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
     $this->assertResponse(422);
-    $error = Json::decode($response);
-    $this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Password</em>.\n");
+    $this->assertEqual($response, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Password</em>.\n");
 
     // Try again with the password.
     $normalized['pass'][0]['existing'] = $account->pass_raw;
@@ -340,7 +336,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.
       ));
