 core/modules/rest/rest.services.yml                |   5 +
 .../EventSubscriber/ResourceResponseSubscriber.php | 210 +++++++++++++++++++++
 core/modules/rest/src/RequestHandler.php           | 111 +----------
 3 files changed, 218 insertions(+), 108 deletions(-)

diff --git a/core/modules/rest/rest.services.yml b/core/modules/rest/rest.services.yml
index 1cc34c1..385c4bd 100644
--- a/core/modules/rest/rest.services.yml
+++ b/core/modules/rest/rest.services.yml
@@ -28,3 +28,8 @@ services:
   logger.channel.rest:
     parent: logger.channel_base
     arguments: ['rest']
+  resource_response.subscriber:
+    class: Drupal\rest\EventSubscriber\ResourceResponseSubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@serializer', '@renderer', '@current_route_match']
diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
new file mode 100644
index 0000000..ba1e3eb
--- /dev/null
+++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -0,0 +1,210 @@
+<?php
+
+namespace Drupal\rest\EventSubscriber;
+
+use Drupal\Core\Cache\CacheableResponse;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\rest\ResourceResponseInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Response subscriber to handle resource responses.
+ */
+class ResourceResponseSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\SerializerInterface
+   */
+  protected $serializer;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs a ResourceResponseSubscriber object.
+   *
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(SerializerInterface $serializer, RendererInterface $renderer, RouteMatchInterface $route_match) {
+    $this->serializer = $serializer;
+    $this->renderer = $renderer;
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * Processes attachments for ResourceResponse responses.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespond(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+    if (!$response instanceof ResourceResponseInterface) {
+      return;
+    }
+
+    $request = $event->getRequest();
+    $format = $this->getResponseFormat($this->routeMatch, $request);
+    $response = $this->renderResponseBody($request, $response, $this->serializer, $format);
+    $event->setResponse($this->flattenResponse($response));
+  }
+
+  /**
+   * Determines the format to respond in.
+   *
+   * Respects the requested format if one is specified. However, it is common to
+   * forget to specify a request format in case of a POST or PATCH. Rather than
+   * simply throwing an error, we apply the robustness principle: when POSTing
+   * or PATCHing using a certain format, you probably expect a response in that
+   * same format.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return string
+   *   The response format.
+   */
+  protected function getResponseFormat(RouteMatchInterface $route_match, Request $request) {
+    $route = $route_match->getRouteObject();
+    $acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
+    $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
+    $acceptable_formats = $request->isMethodSafe() ? $acceptable_request_formats : $acceptable_content_type_formats;
+
+    $requested_format = $request->getRequestFormat();
+    $content_type_format = $request->getContentType();
+
+    // If an acceptable format is requested, then use that. Otherwise, including
+    // and particularly when the client forgot to specify a format, then use
+    // heuristics to select the format that is most likely expected.
+    if (in_array($requested_format, $acceptable_formats)) {
+      return $requested_format;
+    }
+    // If a request body is present, then use the format corresponding to the
+    // request body's Content-Type for the response, if it's an acceptable
+    // format for the request.
+    elseif (!empty($request->getContent()) && in_array($content_type_format, $acceptable_content_type_formats)) {
+      return $content_type_format;
+    }
+    // Otherwise, use the first acceptable format.
+    elseif (!empty($acceptable_formats)) {
+      return $acceptable_formats[0];
+    }
+    // Sometimes, there are no acceptable formats, e.g. DELETE routes.
+    else {
+      return NULL;
+    }
+  }
+
+  /**
+   * Renders a resource response body.
+   *
+   * Serialization can invoke rendering (e.g., generating URLs), but the
+   * serialization API does not provide a mechanism to collect the
+   * bubbleable metadata associated with that (e.g., language and other
+   * contexts), so instead, allow those to "leak" and collect them here in
+   * a render context.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param \Drupal\rest\ResourceResponseInterface $response
+   *   The response from the REST resource.
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer to use.
+   * @param string|null $format
+   *   The response format, or NULL in case the response does not need a format,
+   *   for example for the response to a DELETE request.
+   *
+   * @return \Drupal\rest\ResourceResponse
+   *   The altered response.
+   *
+   * @todo Add test coverage for language negotiation contexts in
+   *   https://www.drupal.org/node/2135829.
+   */
+  protected function renderResponseBody(Request $request, ResourceResponseInterface $response, SerializerInterface $serializer, $format) {
+    $data = $response->getResponseData();
+
+    // If there is data to send, serialize and set it as the response body.
+    if ($data !== NULL) {
+      if ($response instanceof CacheableResponseInterface) {
+        $context = new RenderContext();
+        $output = $this->renderer
+          ->executeInRenderContext($context, function () use ($serializer, $data, $format) {
+            return $serializer->serialize($data, $format);
+          });
+
+        if (!$context->isEmpty()) {
+          $response->addCacheableDependency($context->pop());
+        }
+      }
+      else {
+        $output = $serializer->serialize($data, $format);
+      }
+
+      $response->setContent($output);
+      $response->headers->set('Content-Type', $request->getMimeType($format));
+    }
+
+    return $response;
+  }
+
+  /**
+   * Flattens a fully rendered resource response.
+   *
+   * Ensures that complex data structures in ResourceResponse::getResponseData()
+   * are not serialized.
+   *
+   * @param \Drupal\rest\ResourceResponseInterface $response
+   *   A fully rendered resource response.
+   *
+   * @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response
+   *   The flattened response.
+   */
+  protected function flattenResponse(ResourceResponseInterface $response) {
+    assert('!empty($response->getContent())');
+    $final_response = ($response instanceof CacheableResponseInterface) ? new CacheableResponse() : new Response();
+    $final_response->setContent($response->getContent());
+    $final_response->setStatusCode($response->getStatusCode());
+    $final_response->headers->add($response->headers->all());
+    if ($final_response instanceof CacheableResponseInterface) {
+      $final_response->addCacheableDependency($response->getCacheableMetadata());
+    }
+    return $final_response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::RESPONSE][] = ['onRespond'];
+    return $events;
+  }
+
+}
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index 088dca2..4700b2d 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -2,10 +2,9 @@
 
 namespace Drupal\rest;
 
+use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Cache\CacheableResponseInterface;
-use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@@ -14,10 +13,11 @@
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
-use Symfony\Component\Serializer\SerializerInterface;
 
 /**
  * Acts as intermediate request forwarder for resource plugins.
+ *
+ * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
  */
 class RequestHandler implements ContainerAwareInterface, ContainerInjectionInterface {
 
@@ -129,118 +129,13 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
     }
 
     // Invoke the operation on the resource plugin.
-    $format = $this->getResponseFormat($route_match, $request);
     $response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
 
-    return $response instanceof ResourceResponseInterface ?
-      $this->renderResponse($request, $response, $serializer, $format, $resource_config) :
-      $response;
-  }
-
-  /**
-   * Determines the format to respond in.
-   *
-   * Respects the requested format if one is specified. However, it is common to
-   * forget to specify a request format in case of a POST or PATCH. Rather than
-   * simply throwing an error, we apply the robustness principle: when POSTing
-   * or PATCHing using a certain format, you probably expect a response in that
-   * same format.
-   *
-   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
-   *   The current route match.
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The current request.
-   *
-   * @return string
-   *   The response format.
-   */
-  protected function getResponseFormat(RouteMatchInterface $route_match, Request $request) {
-    $route = $route_match->getRouteObject();
-    $acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
-    $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
-    $acceptable_formats = $request->isMethodSafe() ? $acceptable_request_formats : $acceptable_content_type_formats;
-
-    $requested_format = $request->getRequestFormat();
-    $content_type_format = $request->getContentType();
-
-    // If an acceptable format is requested, then use that. Otherwise, including
-    // and particularly when the client forgot to specify a format, then use
-    // heuristics to select the format that is most likely expected.
-    if (in_array($requested_format, $acceptable_formats)) {
-      return $requested_format;
-    }
-    // If a request body is present, then use the format corresponding to the
-    // request body's Content-Type for the response, if it's an acceptable
-    // format for the request.
-    elseif (!empty($request->getContent()) && in_array($content_type_format, $acceptable_content_type_formats)) {
-      return $content_type_format;
-    }
-    // Otherwise, use the first acceptable format.
-    elseif (!empty($acceptable_formats)) {
-      return $acceptable_formats[0];
-    }
-    // Sometimes, there are no acceptable formats, e.g. DELETE routes.
-    else {
-      return NULL;
-    }
-  }
-
-  /**
-   * Renders a resource response.
-   *
-   * Serialization can invoke rendering (e.g., generating URLs), but the
-   * serialization API does not provide a mechanism to collect the
-   * bubbleable metadata associated with that (e.g., language and other
-   * contexts), so instead, allow those to "leak" and collect them here in
-   * a render context.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   * @param \Drupal\rest\ResourceResponseInterface $response
-   *   The response from the REST resource.
-   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
-   *   The serializer to use.
-   * @param string|null $format
-   *   The response format, or NULL in case the response does not need a format,
-   *   for example for the response to a DELETE request.
-   * @param \Drupal\rest\RestResourceConfigInterface $resource_config
-   *   The resource config.
-   *
-   * @return \Drupal\rest\ResourceResponse
-   *   The altered response.
-   *
-   * @todo Add test coverage for language negotiation contexts in
-   *   https://www.drupal.org/node/2135829.
-   */
-  protected function renderResponse(Request $request, ResourceResponseInterface $response, SerializerInterface $serializer, $format, RestResourceConfigInterface $resource_config) {
-    $data = $response->getResponseData();
-
     if ($response instanceof CacheableResponseInterface) {
       // Add rest config's cache tags.
       $response->addCacheableDependency($resource_config);
     }
 
-    // If there is data to send, serialize and set it as the response body.
-    if ($data !== NULL) {
-      if ($response instanceof CacheableResponseInterface) {
-        $context = new RenderContext();
-        $output = $this->container->get('renderer')
-          ->executeInRenderContext($context, function () use ($serializer, $data, $format) {
-            return $serializer->serialize($data, $format);
-          });
-
-        if (!$context->isEmpty()) {
-          $response->addCacheableDependency($context->pop());
-        }
-      }
-      else {
-        $output = $serializer->serialize($data, $format);
-      }
-
-      $response->setContent($output);
-      $response->headers->set('Content-Type', $request->getMimeType($format));
-    }
-
     return $response;
   }
 
