diff --git a/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
index 1530693..805c0f2 100644
--- a/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
+++ b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
@@ -350,6 +350,12 @@ public function fieldAccess($operation, FieldDefinitionInterface $field_definiti
    *   The access result.
    */
   protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
+    if ($operation === 'edit') {
+      $access = $field_definition->isReadOnly() ? AccessResult::forbidden() : AccessResult::allowed();
+      $access->addCacheableDependency($field_definition);
+      return $access;
+    }
+
     return AccessResult::allowed();
   }
 
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 91ce482..40a2546 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -213,23 +213,15 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
     foreach ($entity->_restSubmittedFields as $field_name) {
       $field = $entity->get($field_name);
 
-      // Entity key fields need special treatment: together they uniquely
-      // identify the entity. Therefore it does not make sense to modify any of
-      // them. However, rather than throwing an error, we just ignore them as
-      // long as their specified values match their current values.
-      if (in_array($field_name, $entity_keys, TRUE)) {
-        // Unchanged values for entity keys don't need access checking.
-        if ($original_entity->get($field_name)->getValue() === $entity->get($field_name)->getValue()) {
-          continue;
-        }
-        // It is not possible to set the language to NULL as it is automatically
-        // re-initialized. As it must not be empty, skip it if it is.
-        elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
-          continue;
-        }
+      // Allow sending read-only fields, as long as their value is unchanged.
+      // This should not be necessary, but due to a design flaw in the Entity
+      // Field API: the validation logic can only use the pre-update values, not
+      // the post-update values.
+      // @see \Drupal\Core\Entity\EntityAccessControlHandlerInterface::fieldAccess()
+      if ($field->getFieldDefinition()->isReadOnly() && $original_entity->get($field_name)->getValue() === $entity->get($field_name)->getValue()) {
+        continue;
       }
-
-      if (!$original_entity->get($field_name)->access('edit')) {
+      elseif (!$original_entity->get($field_name)->access('edit')) {
         throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
       }
       $original_entity->set($field_name, $field->getValue());
diff --git a/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php b/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
index 75e6ccb..3a16972 100644
--- a/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
+++ b/core/modules/user/tests/src/Unit/UserAccessControlHandlerTest.php
@@ -122,6 +122,15 @@ public function assertFieldAccess($field, $viewer, $target, $view, $edit) {
     $field_definition->expects($this->any())
       ->method('getName')
       ->will($this->returnValue($field));
+    $field_definition->expects($this->any())
+      ->method('getCacheContexts')
+      ->will($this->returnValue([]));
+    $field_definition->expects($this->any())
+      ->method('getCacheTags')
+      ->will($this->returnValue([]));
+    $field_definition->expects($this->any())
+      ->method('getCacheMaxAge')
+      ->will($this->returnValue([]));
 
     $this->items
       ->expects($this->any())
