From cdbd063974f645fefad7573ab74042237ff2f286 Mon Sep 17 00:00:00 2001
From: wimleers <wimleers@99777.no-reply.drupal.org>
Date: Sat, 4 Mar 2017 06:51:13 +0100
Subject: [PATCH] fix(Maintainability): SerializableHttpException and
 Drupal\jsonapi\Error\* are not necessary (#2829977 by Wim Leers, dagmar,
 e0ipso, dawehner)

---
 README.md                                     |  6 +++
 jsonapi.services.yml                          |  2 -
 src/Context/FieldResolver.php                 |  8 +--
 src/Controller/EntityResource.php             | 28 +++++-----
 src/Controller/RequestHandler.php             | 19 ++-----
 src/Error/ErrorHandler.php                    | 53 -------------------
 .../DefaultExceptionSubscriber.php            |  3 +-
 .../EntityAccessDeniedHttpException.php       |  6 ++-
 src/Exception/SerializableHttpException.php   | 15 ------
 .../UnprocessableHttpEntityException.php      |  6 ++-
 src/LinkManager/LinkManager.php               |  6 +--
 src/Normalizer/EntityNormalizer.php           |  4 +-
 .../EntityReferenceFieldNormalizer.php        | 12 ++---
 src/Normalizer/HttpExceptionNormalizer.php    |  3 +-
 src/Query/QueryBuilder.php                    |  4 +-
 src/ResourceResponse.php                      |  6 +++
 src/Routing/Param/Filter.php                  |  5 +-
 src/Routing/Param/OffsetPage.php              |  5 +-
 src/Routing/Param/Sort.php                    |  8 +--
 src/Routing/RouteEnhancer.php                 |  4 +-
 .../Kernel/Controller/EntityResourceTest.php  |  2 +-
 tests/src/Unit/Context/FieldResolverTest.php  |  2 +-
 .../src/Unit/LinkManager/LinkManagerTest.php  |  2 +-
 .../EntityReferenceFieldNormalizerTest.php    |  2 +-
 tests/src/Unit/Routing/Param/FilterTest.php   |  2 +-
 .../src/Unit/Routing/Param/OffsetPageTest.php |  2 +-
 tests/src/Unit/Routing/Param/SortTest.php     |  2 +-
 27 files changed, 79 insertions(+), 138 deletions(-)
 delete mode 100644 src/Error/ErrorHandler.php
 delete mode 100644 src/Exception/SerializableHttpException.php

diff --git a/README.md b/README.md
index ab01a11..9ab02e0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
 # JSON API
 The jsonapi module exposes a [JSON API](http://jsonapi.org/) implementation for data stored in Drupal.
 
+The JSON API specification supports [_extensions_](http://jsonapi.org/extensions/). The following extensions are
+supported in this JSON API implementation:
+
+1. [Partial Success](https://gist.github.com/e0ipso/732712c3e573a6af1d83b25b9f0269c8), for when a resource collection is retrieved and only a subset of resources is accessible.
+2. [Fancy Filters](https://gist.github.com/e0ipso/efcc4e96ca2aed58e32948e4f70c2460), to specify the filter strategy exposed by this module.
+
 ## Installation
 
 Install the module as every other module.
diff --git a/jsonapi.services.yml b/jsonapi.services.yml
index c111a8e..3a97096 100644
--- a/jsonapi.services.yml
+++ b/jsonapi.services.yml
@@ -94,8 +94,6 @@ services:
       # Priority 10, to ensure it runs before @paramconverter.entity.
       - { name: paramconverter, priority: 10 }
     arguments: ['@entity.manager']
-  jsonapi.error_handler:
-    class: Drupal\jsonapi\Error\ErrorHandler
   jsonapi.exception_subscriber:
     class: Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber
     tags:
diff --git a/src/Context/FieldResolver.php b/src/Context/FieldResolver.php
index 9eab90d..b0e386c 100644
--- a/src/Context/FieldResolver.php
+++ b/src/Context/FieldResolver.php
@@ -3,7 +3,7 @@
 namespace Drupal\jsonapi\Context;
 
 use Drupal\Core\Entity\EntityFieldManagerInterface;
-use Drupal\jsonapi\Exception\SerializableHttpException;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
  * Service which resolves public field names to and from Drupal field names.
@@ -70,7 +70,7 @@ class FieldResolver {
    */
   public function resolveInternal($external_field_name) {
     if (empty($external_field_name)) {
-      throw new SerializableHttpException(400, 'No field name was provided for the filter.');
+      throw new BadRequestHttpException('No field name was provided for the filter.');
     }
     // Right now we are exposing all the fields with the name they have in
     // the Drupal backend. But this may change in the future.
@@ -85,10 +85,10 @@ class FieldResolver {
     $reference_breadcrumbs = [];
     while ($field_name = array_shift($parts)) {
       if (!$definitions = $this->fieldManager->getFieldStorageDefinitions($entity_type_id)) {
-        throw new SerializableHttpException(400, sprintf('Invalid nested filtering. There is no entity type "%s".', $entity_type_id));
+        throw new BadRequestHttpException(sprintf('Invalid nested filtering. There is no entity type "%s".', $entity_type_id));
       }
       if (empty($definitions[$field_name])) {
-        throw new SerializableHttpException(400, sprintf('Invalid nested filtering. Invalid entity reference "%s".', $field_name));
+        throw new BadRequestHttpException(sprintf('Invalid nested filtering. Invalid entity reference "%s".', $field_name));
       }
       array_push($reference_breadcrumbs, $field_name);
       // Update the entity type with the referenced type.
diff --git a/src/Controller/EntityResource.php b/src/Controller/EntityResource.php
index 363a622..dfd7fee 100644
--- a/src/Controller/EntityResource.php
+++ b/src/Controller/EntityResource.php
@@ -20,7 +20,6 @@ use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
 use Drupal\jsonapi\Resource\EntityCollection;
 use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
 use Drupal\jsonapi\ResourceType\ResourceType;
-use Drupal\jsonapi\Exception\SerializableHttpException;
 use Drupal\jsonapi\Exception\UnprocessableHttpEntityException;
 use Drupal\jsonapi\Query\QueryBuilder;
 use Drupal\jsonapi\Context\CurrentContext;
@@ -29,6 +28,11 @@ use Drupal\jsonapi\Routing\Param\JsonApiParamBase;
 use Drupal\jsonapi\Routing\Param\OffsetPage;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
 
 /**
  * @see \Drupal\jsonapi\Controller\RequestHandler
@@ -131,7 +135,7 @@ class EntityResource {
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity object.
    *
-   * @throws \Drupal\jsonapi\Exception\SerializableHttpException
+   * @throws \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
    *   If validation errors are found.
    */
   protected function validate(EntityInterface $entity) {
@@ -199,7 +203,7 @@ class EntityResource {
     $body = Json::decode($request->getContent());
     $data = $body['data'];
     if ($data['id'] != $entity->uuid()) {
-      throw new SerializableHttpException(400, sprintf(
+      throw new BadRequestHttpException(sprintf(
         'The selected entity (%s) does not match the ID in the payload (%s).',
         $entity->uuid(),
         $data['id']
@@ -296,7 +300,7 @@ class EntityResource {
   public function getRelated(EntityInterface $entity, $related_field, Request $request) {
     /* @var $field_list \Drupal\Core\Field\FieldItemListInterface */
     if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) {
-      throw new SerializableHttpException(404, sprintf('The relationship %s is not present in this resource.', $related_field));
+      throw new NotFoundHttpException(sprintf('The relationship %s is not present in this resource.', $related_field));
     }
     $is_multiple = $field_list
       ->getDataDefinition()
@@ -346,7 +350,7 @@ class EntityResource {
    */
   public function getRelationship(EntityInterface $entity, $related_field, Request $request, $response_code = 200) {
     if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) {
-      throw new SerializableHttpException(404, sprintf('The relationship %s is not present in this resource.', $related_field));
+      throw new NotFoundHttpException(sprintf('The relationship %s is not present in this resource.', $related_field));
     }
     $response = $this->buildWrappedResponse($field_list, $response_code);
     return $response;
@@ -384,7 +388,7 @@ class EntityResource {
       ->getFieldStorageDefinition()
       ->isMultiple();
     if (!$is_multiple) {
-      throw new SerializableHttpException(409, sprintf('You can only POST to to-many relationships. %s is a to-one relationship.', $related_field));
+      throw new ConflictHttpException(sprintf('You can only POST to to-many relationships. %s is a to-one relationship.', $related_field));
     }
 
     $field_access = $field_list->access('edit', NULL, TRUE);
@@ -450,7 +454,7 @@ class EntityResource {
    */
   protected function doPatchIndividualRelationship(EntityInterface $entity, EntityReferenceFieldItemListInterface $parsed_field_list) {
     if ($parsed_field_list->count() > 1) {
-      throw new SerializableHttpException(400, sprintf('Provide a single relationship so to-one relationship fields (%s).', $parsed_field_list->getName()));
+      throw new BadRequestHttpException(sprintf('Provide a single relationship so to-one relationship fields (%s).', $parsed_field_list->getName()));
     }
     $this->doPatchMultipleRelationship($entity, $parsed_field_list);
   }
@@ -497,7 +501,7 @@ class EntityResource {
     }
     if ($parsed_field_list instanceof Request) {
       // This usually means that there was not body provided.
-      throw new SerializableHttpException(400, sprintf('You need to provide a body for DELETE operations on a relationship (%s).', $related_field));
+      throw new BadRequestHttpException(sprintf('You need to provide a body for DELETE operations on a relationship (%s).', $related_field));
     }
     /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list */
     $this->relationshipAccess($entity, $related_field);
@@ -513,7 +517,7 @@ class EntityResource {
       ->getFieldStorageDefinition()
       ->isMultiple();
     if (!$is_multiple) {
-      throw new SerializableHttpException(409, sprintf('You can only DELETE from to-many relationships. %s is a to-one relationship.', $related_field));
+      throw new ConflictHttpException(sprintf('You can only DELETE from to-many relationships. %s is a to-one relationship.', $related_field));
     }
 
     // Compute the list of current values and remove the ones in the payload.
@@ -631,7 +635,7 @@ class EntityResource {
       throw new EntityAccessDeniedHttpException($entity, $entity_access, $related_field, 'The current user is not allowed to update the selected resource.');
     }
     if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) {
-      throw new SerializableHttpException(404, sprintf('The relationship %s is not present in this resource.', $related_field));
+      throw new NotFoundHttpException(sprintf('The relationship %s is not present in this resource.', $related_field));
     }
   }
 
@@ -653,7 +657,7 @@ class EntityResource {
         $destination_field_list = $destination->get($field_name);
       }
       catch (\Exception $e) {
-        throw new SerializableHttpException(400, sprintf('The provided field (%s) does not exist in the entity with ID %s.', $field_name, $destination->uuid()));
+        throw new BadRequestHttpException(sprintf('The provided field (%s) does not exist in the entity with ID %s.', $field_name, $destination->uuid()));
       }
 
       $origin_field_list = $origin->get($field_name);
@@ -670,7 +674,7 @@ class EntityResource {
       $destination->set($field_name, $origin->get($field_name));
     }
     else {
-      throw new SerializableHttpException(400, 'The serialized entity and the destination entity are of different types.');
+      throw new BadRequestHttpException('The serialized entity and the destination entity are of different types.');
     }
   }
 
diff --git a/src/Controller/RequestHandler.php b/src/Controller/RequestHandler.php
index 4c4fb5a..43d767f 100644
--- a/src/Controller/RequestHandler.php
+++ b/src/Controller/RequestHandler.php
@@ -7,14 +7,13 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\jsonapi\Context\CurrentContext;
-use Drupal\jsonapi\Error\ErrorHandler;
-use Drupal\jsonapi\Exception\SerializableHttpException;
 use Drupal\jsonapi\ResourceResponse;
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 use Symfony\Component\Serializer\SerializerInterface;
@@ -85,9 +84,6 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
     // Only add the unserialized data if there is something there.
     $extra_parameters = $unserialized ? [$unserialized, $request] : [$request];
 
-    /** @var \Drupal\jsonapi\Error\ErrorHandler $error_handler */
-    $error_handler = $this->container->get('jsonapi.error_handler');
-    $error_handler->register();
     // Execute the request in context so the cacheable metadata from the entity
     // grants system is caught and added to the response. This is surfaced when
     // executing the underlying entity query.
@@ -100,9 +96,8 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
     if (!$context->isEmpty()) {
       $response->addCacheableDependency($context->pop());
     }
-    $error_handler->restore();
 
-    return $this->renderJsonApiResponse($request, $response, $serializer, $format, $error_handler);
+    return $this->renderJsonApiResponse($request, $response, $serializer, $format);
   }
 
   /**
@@ -122,13 +117,11 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
    *   The serializer to use.
    * @param string $format
    *   The response format.
-   * @param \Drupal\jsonapi\Error\ErrorHandler $error_handler
-   *   The error handler service.
    *
    * @return \Drupal\Core\Cache\CacheableResponseInterface
    *   The altered response.
    */
-  protected function renderJsonApiResponse(Request $request, ResourceResponse $response, SerializerInterface $serializer, $format, ErrorHandler $error_handler) {
+  protected function renderJsonApiResponse(Request $request, ResourceResponse $response, SerializerInterface $serializer, $format) {
     $data = $response->getResponseData();
     $context = new RenderContext();
 
@@ -139,8 +132,6 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
     // HTML generation.
     $cacheable_metadata->addCacheContexts(static::$requiredCacheContexts);
 
-    // Make sure that any PHP error is surfaced as a serializable exception.
-    $error_handler->register();
     $output = $this->container->get('renderer')
       ->executeInRenderContext($context, function () use (
         $serializer,
@@ -155,7 +146,6 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
         // updating the response object's cacheability.
         return $serializer->serialize($data, $format, ['request' => $request, 'cacheable_metadata' => $cacheable_metadata]);
       });
-    $error_handler->restore();
     $response->setContent($output);
     if (!$context->isEmpty()) {
       $response->addCacheableDependency($context->pop());
@@ -200,8 +190,7 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
       ]);
     }
     catch (UnexpectedValueException $e) {
-      throw new SerializableHttpException(
-        422,
+      throw new UnprocessableEntityHttpException(
         sprintf('There was an error un-serializing the data. Message: %s.', $e->getMessage()),
         $e
       );
diff --git a/src/Error/ErrorHandler.php b/src/Error/ErrorHandler.php
deleted file mode 100644
index a51f646..0000000
--- a/src/Error/ErrorHandler.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-namespace Drupal\jsonapi\Error;
-
-/**
- * @see http://jsonapi.org/format/#errors
- *
- * @see \Drupal\jsonapi\Controller\RequestHandler::renderJsonApiResponse
- * @internal
- */
-class ErrorHandler {
-
-  /**
-   * Register the handler.
-   */
-  public function register() {
-    set_error_handler(get_called_class() . '::handle');
-  }
-
-  /**
-   * Go back to normal and restore the previous error handler.
-   */
-  public function restore() {
-    restore_error_handler();
-  }
-
-  /**
-   * Handle the PHP error with custom business logic.
-   *
-   * @param $error_level
-   *   The level of the error raised.
-   * @param $message
-   *   The error message.
-   * @param $filename
-   *   The filename that the error was raised in.
-   * @param $line
-   *   The line number the error was raised at.
-   * @param $context
-   *   An array that points to the active symbol table at the point the error
-   *   occurred.
-   */
-  public static function handle($error_level, $message, $filename, $line, $context) {
-    $message = 'Unexpected PHP error: ' . $message;
-    _drupal_error_handler($error_level, $message, $filename, $line, $context);
-    $types = drupal_error_levels();
-    list($severity_msg, $severity_level) = $types[$error_level];
-    // Only halt execution if the error is more severe than a warning.
-    if ($severity_level < 4) {
-      throw new SerializableHttpException(500, sprintf('[%s] %s', $severity_msg, $message), NULL, [], $error_level);
-    }
-  }
-
-}
diff --git a/src/EventSubscriber/DefaultExceptionSubscriber.php b/src/EventSubscriber/DefaultExceptionSubscriber.php
index 1d2f71a..ea87b35 100644
--- a/src/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/src/EventSubscriber/DefaultExceptionSubscriber.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\jsonapi\EventSubscriber;
 
-use Drupal\jsonapi\Exception\SerializableHttpException;
 use Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber as SerializationDefaultExceptionSubscriber;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
@@ -38,7 +37,7 @@ class DefaultExceptionSubscriber extends SerializationDefaultExceptionSubscriber
       return;
     }
     if (!$exception instanceof HttpException) {
-      $exception = new SerializableHttpException(500, $exception->getMessage(), $exception);
+      $exception = new HttpException(500, $exception->getMessage(), $exception);
       $event->setException($exception);
     }
 
diff --git a/src/Exception/EntityAccessDeniedHttpException.php b/src/Exception/EntityAccessDeniedHttpException.php
index 4db9698..c37d0c6 100644
--- a/src/Exception/EntityAccessDeniedHttpException.php
+++ b/src/Exception/EntityAccessDeniedHttpException.php
@@ -4,9 +4,13 @@ namespace Drupal\jsonapi\Exception;
 
 use Drupal\Core\Access\AccessResultInterface;
 use Drupal\Core\Access\AccessResultReasonInterface;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Entity\EntityInterface;
+use Symfony\Component\HttpKernel\Exception\HttpException;
 
-class EntityAccessDeniedHttpException extends SerializableHttpException {
+class EntityAccessDeniedHttpException extends HttpException {
+
+  use DependencySerializationTrait;
 
   /**
    * The error which caused the 403.
diff --git a/src/Exception/SerializableHttpException.php b/src/Exception/SerializableHttpException.php
deleted file mode 100644
index 3aaa117..0000000
--- a/src/Exception/SerializableHttpException.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Drupal\jsonapi\Exception;
-
-use Drupal\Core\DependencyInjection\DependencySerializationTrait;
-use Symfony\Component\HttpKernel\Exception\HttpException;
-
-/**
- * @internal
- */
-class SerializableHttpException extends HttpException {
-
-  use DependencySerializationTrait;
-
-}
diff --git a/src/Exception/UnprocessableHttpEntityException.php b/src/Exception/UnprocessableHttpEntityException.php
index 7730f5f..e0953c8 100644
--- a/src/Exception/UnprocessableHttpEntityException.php
+++ b/src/Exception/UnprocessableHttpEntityException.php
@@ -3,11 +3,15 @@
 namespace Drupal\jsonapi\Exception;
 
 use Drupal\Core\Entity\EntityConstraintViolationListInterface;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Symfony\Component\HttpKernel\Exception\HttpException;
 
 /**
  * @internal
  */
-class UnprocessableHttpEntityException extends SerializableHttpException {
+class UnprocessableHttpEntityException extends HttpException {
+
+  use DependencySerializationTrait;
 
   /**
    * The constraint violations associated with this exception.
diff --git a/src/LinkManager/LinkManager.php b/src/LinkManager/LinkManager.php
index b3f6d03..2ea7bd1 100644
--- a/src/LinkManager/LinkManager.php
+++ b/src/LinkManager/LinkManager.php
@@ -4,10 +4,10 @@ namespace Drupal\jsonapi\LinkManager;
 
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\jsonapi\ResourceType\ResourceType;
-use Drupal\jsonapi\Exception\SerializableHttpException;
 use Drupal\jsonapi\Routing\Param\OffsetPage;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
 
 /**
@@ -97,7 +97,7 @@ class LinkManager {
    * @param array $link_context
    *   An associative array with extra data to build the links.
    *
-   * @throws \Drupal\jsonapi\Exception\SerializableHttpException
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    *   When the offset and size are invalid.
    *
    * @return string[]
@@ -118,7 +118,7 @@ class LinkManager {
       $size = OffsetPage::$maxSize;
     }
     if ($size <= 0) {
-      throw new SerializableHttpException(400, sprintf('The page size needs to be a positive integer.'));
+      throw new BadRequestHttpException(sprintf('The page size needs to be a positive integer.'));
     }
     $query = (array) $request->query->getIterator();
     $links = [];
diff --git a/src/Normalizer/EntityNormalizer.php b/src/Normalizer/EntityNormalizer.php
index 7958248..de76f47 100644
--- a/src/Normalizer/EntityNormalizer.php
+++ b/src/Normalizer/EntityNormalizer.php
@@ -9,10 +9,10 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Field\EntityReferenceFieldItemList;
 use Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue;
 use Drupal\jsonapi\ResourceType\ResourceType;
-use Drupal\jsonapi\Exception\SerializableHttpException;
 use Drupal\jsonapi\LinkManager\LinkManager;
 use Drupal\jsonapi\Normalizer\Value\NullFieldNormalizerValue;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
@@ -131,7 +131,7 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
    */
   public function denormalize($data, $class, $format = NULL, array $context = array()) {
     if (empty($context['resource_type']) || !$context['resource_type'] instanceof ResourceType) {
-      throw new SerializableHttpException(412, 'Missing context during denormalization.');
+      throw new PreconditionFailedHttpException('Missing context during denormalization.');
     }
     /* @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */
     $resource_type = $context['resource_type'];
diff --git a/src/Normalizer/EntityReferenceFieldNormalizer.php b/src/Normalizer/EntityReferenceFieldNormalizer.php
index 2b21527..024cf6c 100644
--- a/src/Normalizer/EntityReferenceFieldNormalizer.php
+++ b/src/Normalizer/EntityReferenceFieldNormalizer.php
@@ -9,9 +9,9 @@ use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
 use Drupal\jsonapi\Resource\EntityCollection;
-use Drupal\jsonapi\Exception\SerializableHttpException;
 use Drupal\jsonapi\LinkManager\LinkManager;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
  * Normalizer class specific for entity reference field objects.
@@ -112,7 +112,7 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
       $resource_type->getBundle()
     );
     if (empty($context['related']) || empty($field_definitions[$context['related']])) {
-      throw new SerializableHttpException(400, 'Invalid or missing related field.');
+      throw new BadRequestHttpException('Invalid or missing related field.');
     }
     /* @var \Drupal\field\Entity\FieldConfig $field_definition */
     $field_definition = $field_definitions[$context['related']];
@@ -127,7 +127,7 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
       // Make sure that the provided type is compatible with the targeted
       // resource.
       if (!in_array($value['type'], $target_resources)) {
-        throw new SerializableHttpException(400, sprintf(
+        throw new BadRequestHttpException(sprintf(
           'The provided type (%s) does not mach the destination resource types (%s).',
           $value['type'],
           implode(', ', $target_resources)
@@ -159,14 +159,14 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
   protected function massageRelationshipInput($data, $is_multiple) {
     if ($is_multiple) {
       if (!is_array($data['data'])) {
-        throw new SerializableHttpException(400, 'Invalid body payload for the relationship.');
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
       }
       // Leave the invalid elements.
       $invalid_elements = array_filter($data['data'], function ($element) {
         return empty($element['type']) || empty($element['id']);
       });
       if ($invalid_elements) {
-        throw new SerializableHttpException(400, 'Invalid body payload for the relationship.');
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
       }
     }
     else {
@@ -175,7 +175,7 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
         return ['data' => []];
       }
       if (empty($data['data']['type']) || empty($data['data']['id'])) {
-        throw new SerializableHttpException(400, 'Invalid body payload for the relationship.');
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
       }
       $data['data'] = [$data['data']];
     }
diff --git a/src/Normalizer/HttpExceptionNormalizer.php b/src/Normalizer/HttpExceptionNormalizer.php
index 529f862..2468c1c 100644
--- a/src/Normalizer/HttpExceptionNormalizer.php
+++ b/src/Normalizer/HttpExceptionNormalizer.php
@@ -6,7 +6,6 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Session\AccountProxyInterface;
 use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
 use Drupal\jsonapi\Normalizer\Value\HttpExceptionNormalizerValue;
-use Drupal\serialization\Normalizer\NormalizerBase as SerializationNormalizerBase;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 
@@ -16,7 +15,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
  *
  * @see http://jsonapi.org/format/#error-objects
  */
-class HttpExceptionNormalizer extends SerializationNormalizerBase {
+class HttpExceptionNormalizer extends NormalizerBase {
 
   /**
    * The interface or class that this Normalizer supports.
diff --git a/src/Query/QueryBuilder.php b/src/Query/QueryBuilder.php
index 0e84c03..252cbd2 100644
--- a/src/Query/QueryBuilder.php
+++ b/src/Query/QueryBuilder.php
@@ -4,7 +4,6 @@ namespace Drupal\jsonapi\Query;
 
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\jsonapi\Exception\SerializableHttpException;
 use Drupal\jsonapi\Routing\Param\OffsetPage;
 use Drupal\jsonapi\Routing\Param\Filter;
 use Drupal\jsonapi\Routing\Param\JsonApiParamInterface;
@@ -169,8 +168,7 @@ class QueryBuilder {
             break;
 
           default:
-            throw new SerializableHttpException(
-              400,
+            throw new BadRequestHttpException(
               sprintf('Invalid syntax in the filter parameter: %s.', $filter_index)
             );
         };
diff --git a/src/ResourceResponse.php b/src/ResourceResponse.php
index 942d065..6cd61a2 100644
--- a/src/ResourceResponse.php
+++ b/src/ResourceResponse.php
@@ -54,4 +54,10 @@ class ResourceResponse extends Response implements CacheableResponseInterface {
     return $this->responseData;
   }
 
+  // @todo Remove this in https://www.drupal.org/node/2855693
+  public function __sleep() {
+    $this->responseData = NULL;
+    return array_keys(get_object_vars($this));
+  }
+
 }
diff --git a/src/Routing/Param/Filter.php b/src/Routing/Param/Filter.php
index 2033473..4a789d5 100644
--- a/src/Routing/Param/Filter.php
+++ b/src/Routing/Param/Filter.php
@@ -1,7 +1,8 @@
 <?php
 
 namespace Drupal\jsonapi\Routing\Param;
-use Drupal\jsonapi\Exception\SerializableHttpException;
+
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
  * @internal
@@ -68,7 +69,7 @@ class Filter extends JsonApiParamBase {
   protected function expand() {
     // We should always get an array for the filter.
     if (!is_array($this->original)) {
-      throw new SerializableHttpException(400, 'Incorrect value passed to the filter parameter.');
+      throw new BadRequestHttpException('Incorrect value passed to the filter parameter.');
     }
 
     $expanded = [];
diff --git a/src/Routing/Param/OffsetPage.php b/src/Routing/Param/OffsetPage.php
index e2b3feb..238d5fd 100644
--- a/src/Routing/Param/OffsetPage.php
+++ b/src/Routing/Param/OffsetPage.php
@@ -1,7 +1,8 @@
 <?php
 
 namespace Drupal\jsonapi\Routing\Param;
-use Drupal\jsonapi\Exception\SerializableHttpException;
+
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
  * @internal
@@ -41,7 +42,7 @@ class OffsetPage extends JsonApiParamBase {
    */
   protected function expand() {
     if (!is_array($this->original)) {
-      throw new SerializableHttpException(400, 'The page parameter needs to be an array.');
+      throw new BadRequestHttpException('The page parameter needs to be an array.');
     }
     $output = $this->original + ['limit' => static::$maxSize];
     $output['limit'] = $output['limit'] > static::$maxSize ?
diff --git a/src/Routing/Param/Sort.php b/src/Routing/Param/Sort.php
index e0aac10..eee57d9 100644
--- a/src/Routing/Param/Sort.php
+++ b/src/Routing/Param/Sort.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\jsonapi\Routing\Param;
 
-use Drupal\jsonapi\Exception\SerializableHttpException;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
  * @internal
@@ -48,7 +48,7 @@ class Sort extends JsonApiParamBase {
     $sort = $this->original;
 
     if (empty($sort)) {
-      throw new SerializableHttpException(400, 'You need to provide a value for the sort parameter.');
+      throw new BadRequestHttpException('You need to provide a value for the sort parameter.');
     }
 
     // Expand a JSON API compliant sort into a more expressive sort parameter.
@@ -109,7 +109,7 @@ class Sort extends JsonApiParamBase {
     ];
 
     if (!isset($sort_item[static::FIELD_KEY])) {
-      throw new SerializableHttpException(400, 'You need to provide a field name for the sort parameter.');
+      throw new BadRequestHttpException('You need to provide a field name for the sort parameter.');
     }
 
     $expected_keys = [
@@ -122,7 +122,7 @@ class Sort extends JsonApiParamBase {
 
     // Verify correct sort keys.
     if (count(array_diff($expected_keys, array_keys($expanded))) > 0) {
-      throw new SerializableHttpException(400, 'You have provided an invalid set of sort keys.');
+      throw new BadRequestHttpException('You have provided an invalid set of sort keys.');
     }
 
     return $expanded;
diff --git a/src/Routing/RouteEnhancer.php b/src/Routing/RouteEnhancer.php
index 9ee019d..2539602 100644
--- a/src/Routing/RouteEnhancer.php
+++ b/src/Routing/RouteEnhancer.php
@@ -3,9 +3,9 @@
 namespace Drupal\jsonapi\Routing;
 
 use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
-use Drupal\jsonapi\Exception\SerializableHttpException;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\Routing\Route;
 
 /**
@@ -35,7 +35,7 @@ class RouteEnhancer implements RouteEnhancerInterface {
       // If the bundle in the loaded entity does not match the bundle in the
       // route (which is set based on the corresponding ResourceType), then
       // throw an exception.
-      throw new SerializableHttpException(404, sprintf('The loaded entity bundle (%s) does not match the configured resource (%s).', $retrieved_bundle, $configured_bundle));
+      throw new NotFoundHttpException(sprintf('The loaded entity bundle (%s) does not match the configured resource (%s).', $retrieved_bundle, $configured_bundle));
     }
     return $defaults;
   }
diff --git a/tests/src/Kernel/Controller/EntityResourceTest.php b/tests/src/Kernel/Controller/EntityResourceTest.php
index d37dfd3..3fe1876 100644
--- a/tests/src/Kernel/Controller/EntityResourceTest.php
+++ b/tests/src/Kernel/Controller/EntityResourceTest.php
@@ -166,7 +166,7 @@ class EntityResourceTest extends JsonapiKernelTestBase {
 
   /**
    * @covers ::getIndividual
-   * @expectedException \Drupal\jsonapi\Exception\SerializableHttpException
+   * @expectedException \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
    */
   public function testGetIndividualDenied() {
     $role = Role::load(RoleInterface::ANONYMOUS_ID);
diff --git a/tests/src/Unit/Context/FieldResolverTest.php b/tests/src/Unit/Context/FieldResolverTest.php
index 0fe7e6c..488ecea 100644
--- a/tests/src/Unit/Context/FieldResolverTest.php
+++ b/tests/src/Unit/Context/FieldResolverTest.php
@@ -97,7 +97,7 @@ class FieldResolverTest extends UnitTestCase {
    *
    * @covers ::resolveInternal
    *
-   * @expectedException \Drupal\jsonapi\Exception\SerializableHttpException
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    */
   public function testResolveInternalError() {
     $field_manager = $this->prophesize(EntityFieldManagerInterface::class);
diff --git a/tests/src/Unit/LinkManager/LinkManagerTest.php b/tests/src/Unit/LinkManager/LinkManagerTest.php
index 996e59c..eef33fa 100644
--- a/tests/src/Unit/LinkManager/LinkManagerTest.php
+++ b/tests/src/Unit/LinkManager/LinkManagerTest.php
@@ -120,7 +120,7 @@ class LinkManagerTest extends UnitTestCase {
    * Test errors.
    *
    * @covers ::getPagerLinks
-   * @expectedException \Drupal\jsonapi\Exception\SerializableHttpException
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    * @dataProvider getPagerLinksErrorProvider
    */
   public function testGetPagerLinksError($offset, $size, $total, array $pages) {
diff --git a/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php b/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php
index 2f27c5d..4bf2490 100644
--- a/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php
+++ b/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php
@@ -130,7 +130,7 @@ class EntityReferenceFieldNormalizerTest extends UnitTestCase {
 
   /**
    * @covers ::denormalize
-   * @expectedException \Drupal\jsonapi\Exception\SerializableHttpException
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    * @dataProvider denormalizeInvalidResourceProvider
    */
   public function testDenormalizeInvalidResource($data, $field_name) {
diff --git a/tests/src/Unit/Routing/Param/FilterTest.php b/tests/src/Unit/Routing/Param/FilterTest.php
index 15b1928..1763711 100644
--- a/tests/src/Unit/Routing/Param/FilterTest.php
+++ b/tests/src/Unit/Routing/Param/FilterTest.php
@@ -87,7 +87,7 @@ class FilterTest extends UnitTestCase {
 
   /**
    * @covers ::get
-   * @expectedException \Drupal\jsonapi\Exception\SerializableHttpException
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    */
   public function testGetFail() {
     $pager = new Filter(
diff --git a/tests/src/Unit/Routing/Param/OffsetPageTest.php b/tests/src/Unit/Routing/Param/OffsetPageTest.php
index c23e5e0..c4afea5 100644
--- a/tests/src/Unit/Routing/Param/OffsetPageTest.php
+++ b/tests/src/Unit/Routing/Param/OffsetPageTest.php
@@ -35,7 +35,7 @@ class OffsetPageTest extends UnitTestCase {
 
   /**
    * @covers ::get
-   * @expectedException \Drupal\jsonapi\Exception\SerializableHttpException
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    */
   public function testGetFail() {
     $pager = new OffsetPage('lorem');
diff --git a/tests/src/Unit/Routing/Param/SortTest.php b/tests/src/Unit/Routing/Param/SortTest.php
index a407ea5..171f62c 100644
--- a/tests/src/Unit/Routing/Param/SortTest.php
+++ b/tests/src/Unit/Routing/Param/SortTest.php
@@ -52,7 +52,7 @@ class SortTest extends UnitTestCase {
   /**
    * @covers ::get
    * @dataProvider getFailProvider
-   * @expectedException \Drupal\jsonapi\Exception\SerializableHttpException
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    */
   public function testGetFail($input) {
     $sort = new Sort($input);
-- 
GitLab