diff --git a/core/core.services.yml b/core/core.services.yml
index ed1341c..f19e5ff 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1060,7 +1060,7 @@ services:
   html_response.attachments_processor:
     class: Drupal\Core\Render\HtmlResponseAttachmentsProcessor
     tags:
-    arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer']
+    arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler']
   html_response.subscriber:
     class: Drupal\Core\EventSubscriber\HtmlResponseSubscriber
     tags:
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 19beefa..880d7f8 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -24,6 +24,7 @@
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Render\SafeString;
 use Drupal\Core\Render\Renderer;
+use Drupal\Core\Render\HtmlResponseAttachmentsProcessor;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\Response;
@@ -148,62 +149,6 @@
 const LOCALE_PLURAL_DELIMITER = "\03";
 
 /**
- * Adds output to the HEAD tag of the HTML page.
- *
- * This function can be called as long as the headers aren't sent. Pass no
- * arguments (or NULL for both) to retrieve the currently stored elements.
- *
- * @param $data
- *   A renderable array. If the '#type' key is not set then 'html_tag' will be
- *   added as the default '#type'.
- * @param $key
- *   A unique string key to allow implementations of hook_html_head_alter() to
- *   identify the element in $data. Required if $data is not NULL.
- *
- * @return
- *   An array of all stored HEAD elements.
- *
- * @see \Drupal\Core\Render\Element\HtmlTag::preRenderHtmlTag()
- *
- * @deprecated in Drupal 8.0.x, will be removed before Drupal 8.0.0
- *   Use #attached on render arrays.
- */
-function _drupal_add_html_head($data = NULL, $key = NULL) {
-  $stored_head = &drupal_static(__FUNCTION__, array());
-
-  if (isset($data) && isset($key)) {
-    if (!isset($data['#type'])) {
-      $data['#type'] = 'html_tag';
-    }
-    $stored_head[$key] = $data;
-  }
-  return $stored_head;
-}
-
-/**
- * Retrieves output to be displayed in the HEAD tag of the HTML page.
- *
- * @param bool $render
- *   If TRUE render the HEAD elements, otherwise return just the elements.
- *
- * @return string|array
- *   Return the rendered HTML head or the elements itself.
- *
- * @deprecated in Drupal 8.0.x, will be removed before Drupal 8.0.0
- *   Use #attached on render arrays.
- */
-function drupal_get_html_head($render = TRUE) {
-  $elements = _drupal_add_html_head();
-  \Drupal::moduleHandler()->alter('html_head', $elements);
-  if ($render) {
-    return \Drupal::service('renderer')->renderPlain($elements);
-  }
-  else {
-    return $elements;
-  }
-}
-
-/**
  * Prepares a 'destination' URL query parameter for use with url().
  *
  * Used to direct the user back to the referring page after completing a form.
@@ -477,39 +422,6 @@ function base_path() {
 }
 
 /**
- * Adds a LINK tag with a distinct 'rel' attribute to the page's HEAD.
- *
- * This function can be called as long the HTML header hasn't been sent, which
- * on normal pages is up through the preprocess step of _theme('html'). Adding
- * a link will overwrite a prior link with the exact same 'rel' and 'href'
- * attributes.
- *
- * @param $attributes
- *   Associative array of element attributes including 'href' and 'rel'.
- * @param $header
- *   Optional flag to determine if a HTTP 'Link:' header should be sent.
- *
- * @deprecated in Drupal 8.0.x, will be removed before Drupal 8.0.0
- *   Use #attached on render arrays.
- */
-function _drupal_add_html_head_link($attributes, $header = FALSE) {
-  $element = array(
-    '#tag' => 'link',
-    '#attributes' => $attributes,
-  );
-  $href = $attributes['href'];
-
-  if ($header) {
-    // Also add a HTTP header "Link:".
-    $href = '<' . Html::escape($attributes['href']) . '>;';
-    unset($attributes['href']);
-    $element['#attached']['http_header'][] = array('Link',  $href . drupal_http_header_attributes($attributes), TRUE);
-  }
-
-  _drupal_add_html_head($element, 'html_head_link:' . $attributes['rel'] . ':' . $href);
-}
-
-/**
  * Deletes old cached CSS files.
  *
  * @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
@@ -629,39 +541,8 @@ function drupal_merge_attached(array $a, array $b) {
  *   When attaching something of a non-existing attachment type.
  */
 function drupal_process_attached(array $elements) {
-  // Asset attachments are handled by \Drupal\Core\Asset\AssetResolver.
-  foreach (array('library', 'drupalSettings') as $type) {
-    unset($elements['#attached'][$type]);
-  }
-
-  // Add additional types of attachments specified in the render() structure.
-  foreach ($elements['#attached'] as $callback => $options) {
-    foreach ($elements['#attached'][$callback] as $args) {
-      // Limit the amount allowed entries.
-      switch ($callback) {
-        case 'html_head':
-          call_user_func_array('_drupal_add_html_head', $args);
-          break;
-        case 'feed':
-          $args = [[
-            'href' => $args[0],
-            'rel' => 'alternate',
-            'title' => $args[1],
-            'type' => 'application/rss+xml',
-          ]];
-          call_user_func_array('_drupal_add_html_head_link', $args);
-          break;
-        case 'html_head_link':
-          call_user_func_array('_drupal_add_html_head_link', $args);
-          break;
-        case 'http_header':
-          // @todo Remove validation in https://www.drupal.org/node/2477223
-          break;
-        default:
-          throw new \LogicException(sprintf('You are not allowed to use %s in #attached', $callback));
-      }
-    }
-  }
+  $build['#attached'] = $elements['#attached'];
+  \Drupal::service('renderer')->render($build);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php b/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php
index 8257763..c0f9c65 100644
--- a/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php
+++ b/core/lib/Drupal/Core/Render/AttachmentsResponseProcessorInterface.php
@@ -9,6 +9,8 @@
 /**
  * Defines an interface for processing attachments of responses that have them.
  *
+ * @see \Drupal\Core\Ajax\AjaxResponse
+ * @see \Drupal\Core\Ajax\AjaxResponseAttachmentsProcessor
  * @see \Drupal\Core\Render\HtmlResponse
  * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
  */
@@ -17,13 +19,38 @@
   /**
    * Processes the attachments of a response that has attachments.
    *
+   * Libraries, JavaScript settings, feeds, HTML <head> tags, HTML <head> links,
+   * HTTP headers, and the HTTP status code are attached to render arrays using
+   * the #attached property. The #attached property is an associative array,
+   * where the keys are the attachment types and the values are the attached
+   * data. For example:
+   *
+   * @code
+   * $build['#attached']['library'][] = [
+   *   'library' => ['core/jquery']
+   * ];
+   * $build['#attached']['http_header'][] = [
+   *   ['Content-Type', 'application/rss+xml; charset=utf-8'],
+   * ];
+   * @endcode
+   *
+   * The available keys are:
+   * - 'library' (asset libraries)
+   * - 'drupalSettings' (JavaScript settings)
+   * - 'feed' (RSS feeds)
+   * - 'html_head' (tags in HTML <head>)
+   * - 'html_head_link' (<link> tags in HTML <head>)
+   * - 'http_header' (HTTP headers and status code)
+   *
    * @param \Drupal\Core\Render\AttachmentsInterface $response
-   *   The response to process the attachments for.
+   *   The response to process.
    *
    * @return \Drupal\Core\Render\AttachmentsInterface
    *   The processed response.
    *
    * @throws \InvalidArgumentException
+   *   Thrown when the $response parameter is not a
+   *   Drupal\Core\Render\HtmlResponse.
    */
   public function processAttachments(AttachmentsInterface $response);
 
diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
index 19edef1..6104b0a 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
@@ -11,11 +11,23 @@
 use Drupal\Core\Asset\AttachedAssets;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\EnforcedResponseException;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\SafeMarkup;
 use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Processes attachments of HTML responses.
  *
+ * This class is used by the rendering service to process the #attached part of
+ * the render array, for HTML responses.
+ *
+ * To render attachments to HTML for testing without a controller, use the
+ * 'bare_html_page_renderer' service to generate a
+ * Drupal\Core\Render\HtmlResponse object. Then use its getContent(),
+ * getStatusCode(), and/or the headers property to access the result.
+ *
  * @see template_preprocess_html()
  * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface
  * @see \Drupal\Core\Render\BareHtmlPageRenderer
@@ -67,6 +79,13 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
   protected $renderer;
 
   /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
    * Constructs a HtmlResponseAttachmentsProcessor object.
    *
    * @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
@@ -81,14 +100,17 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
    *   The request stack.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
    */
-  public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer) {
+  public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
     $this->assetResolver = $asset_resolver;
     $this->config = $config_factory->get('system.performance');
     $this->cssCollectionRenderer = $css_collection_renderer;
     $this->jsCollectionRenderer = $js_collection_renderer;
     $this->requestStack = $request_stack;
     $this->renderer = $renderer;
+    $this->moduleHandler = $module_handler;
   }
 
   /**
@@ -117,27 +139,67 @@ public function processAttachments(AttachmentsInterface $response) {
       return $e->getResponse();
     }
 
+    // Get a reference to the attachments.
     $attached = $response->getAttachments();
 
+    // Send a message back if the render array has unsupported #attached types.
+    $unsupported_types = array_diff(
+      array_keys($attached),
+      ['html_head', 'feed', 'html_head_link', 'http_header', 'library', 'html_response_attachment_placeholders', 'placeholders', 'drupalSettings']
+    );
+    if (!empty($unsupported_types)) {
+      throw new \LogicException(sprintf('You are not allowed to use %s in #attached.', implode(', ', $unsupported_types)));
+    }
+
     // Get the placeholders from attached and then remove them.
     $attachment_placeholders = $attached['html_response_attachment_placeholders'];
     unset($attached['html_response_attachment_placeholders']);
 
     $variables = $this->processAssetLibraries($attached, $attachment_placeholders);
 
-    // Handle all non-asset attachments. This populates drupal_get_html_head().
-    $all_attached = ['#attached' => $attached];
-    drupal_process_attached($all_attached);
+    // Since we can only replace content in the HTML head section if there's a
+    // placeholder for it, we can safely avoid processing the render array if
+    // it's not present.
+    if (!empty($attachment_placeholders['head'])) {
+      // 'feed' is a special case of 'html_head_link'. We process them into
+      // 'html_head_link' entries and merge them.
+      if (!empty($attached['feed'])) {
+        $attached = BubbleableMetadata::mergeAttachments(
+          $attached,
+          $this->processFeed($attached['feed'])
+        );
+      }
+      // 'html_head_link' is a special case of 'html_head' which can be present
+      // as a head element, but also as a Link: HTTP header depending on
+      // settings in the render array. Processing it can add to both the
+      // 'html_head' and 'http_header' keys of '#attached', so we must address
+      // it before 'html_head'.
+      if (!empty($attached['html_head_link'])) {
+        // Merge the processed 'html_head_link' into $attached so that its
+        // 'html_head' and 'http_header' values are present for further
+        // processing.
+        $attached = BubbleableMetadata::mergeAttachments(
+          $attached,
+          $this->processHtmlHeadLink($attached['html_head_link'])
+        );
+      }
 
-    // Get HTML head elements - if present.
-    if (isset($attachment_placeholders['head'])) {
-      $variables['head'] = drupal_get_html_head(FALSE);
+      // Now we can process 'html_head', which contains both 'feed' and
+      // 'html_head_link'.
+      if (!empty($attached['html_head'])) {
+        $html_head = $this->processHtmlHead($attached['html_head']);
+        // Invoke hook_html_head_alter().
+        $this->moduleHandler->alter('html_head', $html_head);
+        // Store the result in $variables so it can be inserted into the
+        // placeholder.
+        $variables['head'] = $html_head;
+      }
     }
 
     // Now replace the attachment placeholders.
     $this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables);
 
-    // Finally set the headers on the response if any bubbled.
+    // Set the HTTP headers and status code on the response if any bubbled.
     if (!empty($attached['http_header'])) {
       $this->setHeaders($response, $attached['http_header']);
     }
@@ -243,6 +305,9 @@ protected function processAssetLibraries(array $attached, array $placeholders) {
   /**
    * Renders HTML response attachment placeholders.
    *
+   * This is the last step where all of the attachments are placed into the
+   * response object's contents.
+   *
    * @param \Drupal\Core\Render\HtmlResponse $response
    *   The HTML response to update.
    * @param array $placeholders
@@ -268,7 +333,13 @@ protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $respon
    * @param \Drupal\Core\Render\HtmlResponse $response
    *   The HTML response to update.
    * @param array $headers
-   *   The headers to set.
+   *   The headers to set, as an array. The items in this array should be as
+   *   follows:
+   *   - The header name.
+   *   - The header value.
+   *   - (optional) Whether to replace a current value with the new one, or add
+   *     it to the others. If the value is not replaced, it will be appended,
+   *     resulting in a header like this: 'Header: value1,value2'
    */
   protected function setHeaders(HtmlResponse $response, array $headers) {
     foreach ($headers as $values) {
@@ -281,8 +352,105 @@ protected function setHeaders(HtmlResponse $response, array $headers) {
       if (strtolower($name) === 'status') {
         $response->setStatusCode($value);
       }
-      $response->headers->set($name, $value, $replace);
+      else {
+        $response->headers->set($name, $value, $replace);
+      }
+    }
+  }
+
+  /**
+   * Ensure proper key/data order and defaults for renderable head items.
+   *
+   * @param array $html_head
+   *   The ['#attached']['html_head'] portion of a render array.
+   *
+   * @return array
+   *   The ['#attached']['html_head'] portion of a render array with #type of
+   *   html_tag added for items without a #type.
+   */
+  protected function processHtmlHead(array $html_head) {
+    $head = [];
+    foreach ($html_head as $item) {
+      list($data, $key) = $item;
+      if (!isset($data['#type'])) {
+        $data['#type'] = 'html_tag';
+      }
+      $head[$key] = $data;
+    }
+    return $head;
+  }
+
+  /**
+   * Transform a html_head_link array into html_head and http_header arrays.
+   *
+   * html_head_link is a special case of html_head which can be present as
+   * a link item in the HTML head section, and also as a Link: HTTP header,
+   * depending on options in the render array. Processing it can add to both the
+   * html_head and http_header sections.
+   *
+   * @param array $html_head_link
+   *   The 'html_head_link' value of a render array. Each head link is specified
+   *   by a two-element array:
+   *   - An array specifying the attributes of the link.
+   *   - A boolean specifying whether the link should also be a Link: HTTP
+   *     header.
+   *
+   * @return array
+   *   An ['#attached'] section of a render array. This allows us to easily
+   *   merge the results with other render arrays. The array could contain the
+   *   following keys:
+   *   - http_header
+   *   - html_head
+   */
+  protected function processHtmlHeadLink(array $html_head_link) {
+    $attached = [];
+
+    foreach ($html_head_link as $item) {
+      $attributes = $item[0];
+      $should_add_header = isset($item[1]) ? $item[1] : FALSE;
+
+      $element = array(
+        '#tag' => 'link',
+        '#attributes' => $attributes,
+      );
+      $href = $attributes['href'];
+      $attached['html_head'][] = [$element, 'html_head_link:' . $attributes['rel'] . ':' . $href];
+
+      if ($should_add_header) {
+        // Also add a HTTP header "Link:".
+        $href = '<' . Html::escape($attributes['href'] . '>');
+        unset($attributes['href']);
+        $attached['http_header'][] = ['Link', $href . drupal_http_header_attributes($attributes), TRUE];
+      }
+    }
+    return $attached;
+  }
+
+  /**
+   * Transform a 'feed' attachment into an 'html_head_link' attachment.
+   *
+   * The RSS feed is a special case of 'html_head_link', so we just turn it into
+   * one.
+   *
+   * @param array $attached_feed
+   *   The ['#attached']['feed'] portion of a render array.
+   *
+   * @return array
+   *   An ['#attached']['html_head_link'] array, suitable for merging with
+   *   another 'html_head_link' array.
+   */
+  protected function processFeed($attached_feed) {
+    $html_head_link = [];
+    foreach($attached_feed as $item) {
+      $feed_link = [
+        'href' => $item[0],
+        'rel' => 'alternate',
+        'title' => empty($item[1]) ? '' : $item[1],
+        'type' => 'application/rss+xml',
+      ];
+      $html_head_link[] = [$feed_link, FALSE];
     }
+    return ['html_head_link' => $html_head_link];
   }
 
 }
diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php
index e9144cf..3139224 100644
--- a/core/lib/Drupal/Core/Render/theme.api.php
+++ b/core/lib/Drupal/Core/Render/theme.api.php
@@ -375,7 +375,10 @@
  * Libraries, JavaScript settings, feeds, HTML <head> tags and HTML <head> links
  * are attached to elements using the #attached property. The #attached property
  * is an associative array, where the keys are the attachment types and the
- * values are the attached data. For example:
+ * values are the attached data.
+ *
+ * The #attached property can also be used to specify HTTP headers and the
+ * response status code.
  *
  * The #attached property allows loading of asset libraries (which may contain
  * CSS assets, JavaScript assets, and JavaScript setting assets), JavaScript
@@ -386,10 +389,11 @@
  * @code
  * $build['#attached']['library'][] = 'core/jquery';
  * $build['#attached']['drupalSettings']['foo'] = 'bar';
- * $build['#attached']['feed'][] = ['aggregator/rss', $this->t('Feed title')];
+ * $build['#attached']['feed'][] = [$url, $this->t('Feed title')];
  * @endcode
  *
- * See drupal_process_attached() for additional information.
+ * See \Drupal\Core\Render\AttachmentsResponseProcessorInterface for additional
+ * information.
  *
  * See \Drupal\Core\Asset\LibraryDiscoveryParser::parseLibraryInfo() for more
  * information on how to define libraries.
diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index 3a48671..dc543fc 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -466,7 +466,6 @@ function template_preprocess_book_export_html(&$variables) {
   $variables['base_url'] = $base_url;
   $variables['language'] = $language_interface;
   $variables['language_rtl'] = ($language_interface->getDirection() == LanguageInterface::DIRECTION_RTL);
-  $variables['head'] = drupal_get_html_head();
 
   // HTML element attributes.
   $attributes = array();
diff --git a/core/modules/system/src/Tests/Common/AddFeedTest.php b/core/modules/system/src/Tests/Common/AddFeedTest.php
index a83f41b..7ec728e 100644
--- a/core/modules/system/src/Tests/Common/AddFeedTest.php
+++ b/core/modules/system/src/Tests/Common/AddFeedTest.php
@@ -61,9 +61,15 @@ function testBasicFeedAddNoTitle() {
       $build['#attached']['feed'][] = [$feed_info['url'], $feed_info['title']];
     }
 
-    drupal_process_attached($build);
+    // Use the bare HTML page renderer to render our links.
+    $renderer = $this->container->get('bare_html_page_renderer');
+    $response = $renderer->renderBarePage(
+      $build, '', $this->container->get('theme.manager')->getActiveTheme()->getName()
+    );
 
-    $this->setRawContent(drupal_get_html_head());
+    // Glean the content from the response object.
+    $this->setRawContent($response->getContent());
+    // Assert that the content contains the RSS links we specified.
     foreach ($urls as $description => $feed_info) {
       $this->assertPattern($this->urlToRSSLinkPattern($feed_info['url'], $feed_info['title']), format_string('Found correct feed header for %description', array('%description' => $description)));
     }
diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php
index 11bb96a..a6aa89e 100644
--- a/core/modules/system/src/Tests/Common/RenderTest.php
+++ b/core/modules/system/src/Tests/Common/RenderTest.php
@@ -53,14 +53,15 @@ function testDrupalRenderThemePreprocessAttached() {
   }
 
   /**
-   * Tests drupal_process_attached().
+   * Tests whether the render process allows invalid attachment types.
    */
-  public function testDrupalProcessAttached() {
+  public function testProcessAttached() {
     // Specify invalid attachments in a render array.
     $build['#attached']['library'][] = 'core/drupal.states';
     $build['#attached']['drupal_process_states'][] = [];
+    $renderer = $this->container->get('bare_html_page_renderer');
     try {
-      drupal_process_attached($build);
+      $renderer->renderBarePage($build, '', $this->container->get('theme.manager')->getActiveTheme()->getName());
       $this->fail("Invalid #attachment 'drupal_process_states' allowed");
     }
     catch (\Exception $e) {
diff --git a/core/modules/system/src/Tests/HttpKernel/HeadersResponseCodeRenderTest.php b/core/modules/system/src/Tests/HttpKernel/HeadersResponseCodeRenderTest.php
deleted file mode 100644
index 7a81a0d..0000000
--- a/core/modules/system/src/Tests/HttpKernel/HeadersResponseCodeRenderTest.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\system\Tests\HttpKernel\HeadersResponseCodeRenderTest.
- */
-
-namespace Drupal\system\Tests\HttpKernel;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests rendering headers and response codes.
- *
- * @group Routing
- */
-class HeadersResponseCodeRenderTest extends WebTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('httpkernel_test');
-
-  /**
-   * Tests the rendering of an array-based header and response code.
-   */
-  public function testHeaderResponseCode() {
-    $this->drupalGet('/httpkernel-test/teapot');
-    $this->assertResponse(418);
-    $this->assertHeader('X-Test-Teapot', 'Teapot Mode Active');
-    $this->assertHeader('X-Test-Teapot-Replace', 'Teapot replaced');
-    $this->assertHeader('X-Test-Teapot-No-Replace', 'This value is not replaced,This one is added');
-  }
-
-}
diff --git a/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php b/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php
new file mode 100644
index 0000000..d95b3fb
--- /dev/null
+++ b/core/modules/system/src/Tests/Render/HtmlResponseAttachmentsTest.php
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Render\HtmlResponseAttachmentsTest.
+ */
+
+namespace Drupal\system\Tests\Render;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Functional tests for HtmlResponseAttachmentsProcessor.
+ *
+ * @group Render
+ */
+class HtmlResponseAttachmentsTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['render_attached_test'];
+
+  /**
+   * Test rendering of ['#attached'].
+   */
+  public function testAttachments() {
+    // Test ['#attached']['http_header] = ['Status', $code].
+    $this->drupalGet('/render_attached_test/teapot');
+    $this->assertResponse(418);
+    $this->assertHeader('X-Drupal-Cache', 'MISS');
+    // Repeat for the cache.
+    $this->drupalGet('/render_attached_test/teapot');
+    $this->assertResponse(418);
+    $this->assertHeader('X-Drupal-Cache', 'HIT');
+
+    // Test ['#attached']['http_header'] with various replacement rules.
+    $this->drupalGet('/render_attached_test/header');
+    $this->assertTeapotHeaders();
+    $this->assertHeader('X-Drupal-Cache', 'MISS');
+    // Repeat for the cache.
+    $this->drupalGet('/render_attached_test/header');
+    $this->assertHeader('X-Drupal-Cache', 'HIT');
+
+    // Test ['#attached']['feed'].
+    $this->drupalGet('/render_attached_test/feed');
+    $this->assertHeader('X-Drupal-Cache', 'MISS');
+    $this->assertFeed();
+    // Repeat for the cache.
+    $this->drupalGet('/render_attached_test/feed');
+    $this->assertHeader('X-Drupal-Cache', 'HIT');
+
+    // Test ['#attached']['html_head'].
+    $this->drupalGet('/render_attached_test/head');
+    $this->assertHeader('X-Drupal-Cache', 'MISS');
+    $this->assertHead();
+    // Repeat for the cache.
+    $this->drupalGet('/render_attached_test/head');
+    $this->assertHeader('X-Drupal-Cache', 'HIT');
+
+    // Now repeat all of the preceeding using drupal_process_attached().
+    // Test ['#attached']['http_header] = ['Status', $code].
+    $this->drupalGet('/render_attached_test/teapot_dpa');
+    $this->assertResponse(418);
+    $this->assertHeader('X-Drupal-Cache', 'MISS');
+    // Repeat for the cache.
+    $this->drupalGet('/render_attached_test/teapot_dpa');
+    $this->assertResponse(418);
+    $this->assertHeader('X-Drupal-Cache', 'HIT');
+
+    // Test ['#attached']['http_header'] with various replacement rules.
+    $this->drupalGet('/render_attached_test/header_dpa');
+    $this->assertHeader('X-Drupal-Cache', 'MISS');
+    $this->assertTeapotHeaders();
+    // Repeat for the cache.
+    $this->drupalGet('/render_attached_test/header_dpa');
+    $this->assertHeader('X-Drupal-Cache', 'HIT');
+
+    // Test ['#attached']['feed'].
+    $this->drupalGet('/render_attached_test/feed_dpa');
+    $this->assertHeader('X-Drupal-Cache', 'MISS');
+    $this->assertFeed();
+    // Repeat for the cache.
+    $this->drupalGet('/render_attached_test/feed_dpa');
+    $this->assertHeader('X-Drupal-Cache', 'HIT');
+
+    // Test ['#attached']['html_head'].
+    $this->drupalGet('/render_attached_test/head_dpa');
+    $this->assertHeader('X-Drupal-Cache', 'MISS');
+    $this->assertHead();
+    // Repeat for the cache.
+    $this->drupalGet('/render_attached_test/head_dpa');
+    $this->assertHeader('X-Drupal-Cache', 'HIT');
+  }
+
+  /**
+   * Test caching of ['#attached'].
+   */
+  public function testSmartCache() {
+    // Make sure our test block is visible.
+    $this->drupalPlaceBlock('drupal_process_attached_block', ['region' => 'content']);
+
+    // Get the front page, which should now have our visible block.
+    $this->drupalGet('');
+    // Make sure our block is visible.
+    $this->assertText('Headers handled by drupal_process_attached().');
+    // Test that all our attached items are present.
+    $this->assertFeed();
+    $this->assertHead();
+    $this->assertResponse(418);
+    $this->assertTeapotHeaders();
+
+    // Reload the page, to test caching.
+    $this->drupalGet('');
+    // Make sure our block is visible.
+    $this->assertText('Headers handled by drupal_process_attached().');
+    // The header should be present again.
+    $this->assertHeader('X-Test-Teapot', 'Teapot Mode Active');
+  }
+
+  /**
+   * Helper function to make assertions about added HTTP headers.
+   */
+  protected function assertTeapotHeaders() {
+    $this->assertHeader('X-Test-Teapot', 'Teapot Mode Active');
+    $this->assertHeader('X-Test-Teapot-Replace', 'Teapot replaced');
+    $this->assertHeader('X-Test-Teapot-No-Replace', 'This value is not replaced,This one is added');
+  }
+
+  /**
+   * Helper function to make assertions about the presence of an RSS feed.
+   */
+  protected function assertFeed() {
+    // Discover the DOM element for the feed link.
+    $test_meta = $this->xpath('//head/link[@href="test://url"]');
+    $this->assertEqual(1, count($test_meta), 'Link has URL.');
+    // Reconcile the other attributes.
+    $test_meta_attributes = [
+      'href' => 'test://url',
+      'rel' => 'alternate',
+      'type' => 'application/rss+xml',
+      'title' => 'Your RSS feed.',
+    ];
+    $test_meta = reset($test_meta);
+    foreach ($test_meta->attributes() as $attribute => $value) {
+      $this->assertEqual($value, $test_meta_attributes[$attribute]);
+    }
+  }
+
+  /**
+   * Helper function to make assertions about HTML head elements.
+   */
+  protected function assertHead() {
+    // Discover the DOM element for the meta link.
+    $test_meta = $this->xpath('//head/meta[@test-attribute="testy"]');
+    $this->assertEqual(1, count($test_meta), 'There\'s only one test attribute.');
+    // Grab the only DOM element.
+    $test_meta = reset($test_meta);
+    $test_meta_attributes = $test_meta->attributes();
+    $this->assertEqual($test_meta_attributes['test-attribute'], 'testy');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/httpkernel_test/httpkernel_test.routing.yml b/core/modules/system/tests/modules/httpkernel_test/httpkernel_test.routing.yml
index 8f5762a..7de4338 100644
--- a/core/modules/system/tests/modules/httpkernel_test/httpkernel_test.routing.yml
+++ b/core/modules/system/tests/modules/httpkernel_test/httpkernel_test.routing.yml
@@ -4,9 +4,3 @@ httpkernel_test.empty:
     _controller: '\Drupal\httpkernel_test\Controller\TestController::get'
   requirements:
     _access: 'TRUE'
-httpkernel_test.teapot:
-  path: '/httpkernel-test/teapot'
-  defaults:
-    _controller: '\Drupal\httpkernel_test\Controller\TestController::teapot'
-  requirements:
-    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/httpkernel_test/src/Controller/TestController.php b/core/modules/system/tests/modules/httpkernel_test/src/Controller/TestController.php
index 5730080..1699594 100644
--- a/core/modules/system/tests/modules/httpkernel_test/src/Controller/TestController.php
+++ b/core/modules/system/tests/modules/httpkernel_test/src/Controller/TestController.php
@@ -21,21 +21,4 @@ public function get() {
     return new Response();
   }
 
-  /**
-   * Test special header and status code rendering.
-   *
-   * @return array
-   *   A render array using features of the 'http_header' directive.
-   */
-  public function teapot() {
-    $render = [];
-    $render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'This value gets replaced'];
-    $render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'Teapot replaced', TRUE];
-    $render['#attached']['http_header'][] = ['X-Test-Teapot-No-Replace', 'This value is not replaced'];
-    $render['#attached']['http_header'][] = ['X-Test-Teapot-No-Replace', 'This one is added', FALSE];
-    $render['#attached']['http_header'][] = ['X-Test-Teapot', 'Teapot Mode Active'];
-    $render['#attached']['http_header'][] = ['Status', "418 I'm a teapot."];
-    return $render;
-  }
-
 }
diff --git a/core/modules/system/tests/modules/render_attached_test/render_attached_test.info.yml b/core/modules/system/tests/modules/render_attached_test/render_attached_test.info.yml
new file mode 100644
index 0000000..111fb96
--- /dev/null
+++ b/core/modules/system/tests/modules/render_attached_test/render_attached_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Rendering #attached test'
+type: module
+description: 'Support module for HtmlResponseAttachmentsTest.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - block
diff --git a/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml b/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml
new file mode 100644
index 0000000..d3d578b
--- /dev/null
+++ b/core/modules/system/tests/modules/render_attached_test/render_attached_test.routing.yml
@@ -0,0 +1,55 @@
+render_attached.teapot:
+  path: '/render_attached_test/teapot'
+  defaults:
+    _controller: '\Drupal\render_attached_test\Controller\TestController::teapotHeaderStatus'
+  requirements:
+    _access: 'TRUE'
+
+render_attached.header:
+  path: '/render_attached_test/header'
+  defaults:
+    _controller: '\Drupal\render_attached_test\Controller\TestController::header'
+  requirements:
+    _access: 'TRUE'
+
+render_attached.head:
+  path: '/render_attached_test/head'
+  defaults:
+    _controller: '\Drupal\render_attached_test\Controller\TestController::head'
+  requirements:
+    _access: 'TRUE'
+
+render_attached.feed_single:
+  path: '/render_attached_test/feed'
+  defaults:
+    _controller: '\Drupal\render_attached_test\Controller\TestController::feed'
+  requirements:
+    _access: 'TRUE'
+
+render_attached.teapot_dpa:
+  path: '/render_attached_test/teapot_dpa'
+  defaults:
+    _controller: '\Drupal\render_attached_test\Controller\TestController::teapotHeaderStatusDpa'
+  requirements:
+    _access: 'TRUE'
+
+render_attached.header_dpa:
+  path: '/render_attached_test/header_dpa'
+  defaults:
+    _controller: '\Drupal\render_attached_test\Controller\TestController::headerDpa'
+  requirements:
+    _access: 'TRUE'
+
+render_attached.head_dpa:
+  path: '/render_attached_test/head_dpa'
+  defaults:
+    _controller: '\Drupal\render_attached_test\Controller\TestController::headDpa'
+  requirements:
+    _access: 'TRUE'
+
+render_attached.feed_single_dpa:
+  path: '/render_attached_test/feed_dpa'
+  defaults:
+    _controller: '\Drupal\render_attached_test\Controller\TestController::feedDpa'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/render_attached_test/src/Controller/TestController.php b/core/modules/system/tests/modules/render_attached_test/src/Controller/TestController.php
new file mode 100644
index 0000000..3d158b3
--- /dev/null
+++ b/core/modules/system/tests/modules/render_attached_test/src/Controller/TestController.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\render_attached_test\Controller\TestController.
+ */
+
+namespace Drupal\render_attached_test\Controller;
+
+/**
+ * Controller for various permutations of #attached in the render array.
+ */
+class TestController {
+
+  /**
+   * Test special header and status code rendering.
+   *
+   * @return array
+   *   A render array using features of the 'http_header' directive.
+   */
+  public function teapotHeaderStatus() {
+    $render = [];
+    $render['#attached']['http_header'][] = ['Status', "418 I'm a teapot."];
+    return $render;
+  }
+
+  /**
+   * Test attached HTML head rendering.
+   *
+   * @return array
+   *   A render array using the 'http_head' directive.
+   */
+  public function header() {
+    $render = [];
+    $render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'This value gets replaced'];
+    $render['#attached']['http_header'][] = ['X-Test-Teapot-Replace', 'Teapot replaced', TRUE];
+    $render['#attached']['http_header'][] = ['X-Test-Teapot-No-Replace', 'This value is not replaced'];
+    $render['#attached']['http_header'][] = ['X-Test-Teapot-No-Replace', 'This one is added', FALSE];
+    $render['#attached']['http_header'][] = ['X-Test-Teapot', 'Teapot Mode Active'];
+    return $render;
+  }
+
+  /**
+   * Test attached HTML head rendering.
+   *
+   * @return array
+   *   A render array using the 'html_head' directive.
+   */
+  public function head() {
+    $head = [
+      [
+        '#tag' => 'meta',
+        '#attributes' => [
+          'test-attribute' => 'testy',
+        ],
+      ],
+      'test_head_attribute',
+    ];
+
+    $render = [];
+    $render['#attached']['html_head'][] = $head;
+    return $render;
+  }
+
+  /**
+   * Test attached feed rendering.
+   *
+   * @return array
+   *   A render array using the 'feed' directive.
+   */
+  public function feed() {
+    $render = [];
+    $render['#attached']['feed'][] = ['test://url', 'Your RSS feed.'];
+    return $render;
+  }
+
+  /**
+   * Test special header and status code rendering as a side-effect.
+   *
+   * @return array
+   *   A generic render array.
+   */
+  public function teapotHeaderStatusDpa() {
+    \drupal_process_attached($this->teapotHeaderStatus());
+    return ['#markup' => "I'm some markup here to fool the kernel into rendering this page."];
+  }
+
+  /**
+   * Test attached HTML head rendering as a side-effect.
+   *
+   * @return array
+   *   A render array using the 'http_header' directive.
+   */
+  public function headerDpa() {
+    \drupal_process_attached($this->header());
+    return ['#markup' => "I'm some markup here to fool the kernel into rendering this page."];
+  }
+
+  /**
+   * Test attached HTML head rendering as a side-effect.
+   *
+   * @return array
+   *   A render array using the 'html_head' directive.
+   */
+  public function headDpa() {
+    \drupal_process_attached($this->head());
+    return ['#markup' => "I'm some markup here to fool the kernel into rendering this page."];
+  }
+
+  /**
+   * Test attached feed rendering as a side-effect.
+   *
+   * @return array
+   *   A render array using the 'feed' directive.
+   */
+  public function feedDpa() {
+    \drupal_process_attached($this->feed());
+    return ['#markup' => "I'm some markup here to fool the kernel into rendering this page."];
+  }
+
+}
diff --git a/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/DrupalProcessAttachedBlock.php b/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/DrupalProcessAttachedBlock.php
new file mode 100644
index 0000000..3e7b342
--- /dev/null
+++ b/core/modules/system/tests/modules/render_attached_test/src/Plugin/Block/DrupalProcessAttachedBlock.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\render_attached_test\Plugin\Block\DrupalProcessAttachedBlock.
+ */
+
+namespace Drupal\render_attached_test\Plugin\Block;
+
+use Drupal\render_attached_test\Controller\TestController;
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Render\BubbleableMetadata;
+
+/**
+ * A block we can use to test caching of #attached headers.
+ *
+ * @Block(
+ *   id = "drupal_process_attached_block",
+ *   admin_label = @Translation("DrupalProcessAttachedBlock")
+ * )
+ *
+ * @see \Drupal\system\Tests\Render\HtmlResponseAttachmentsTest
+ */
+class DrupalProcessAttachedBlock extends BlockBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    // Grab test attachment fixtures from
+    // Drupal\render_attached_test\Controller\TestController.
+    $controller = new TestController();
+    $attached = BubbleableMetadata::mergeAttachments($controller->feed(), $controller->head());
+    $attached = BubbleableMetadata::mergeAttachments($attached, $controller->header());
+    $attached = BubbleableMetadata::mergeAttachments($attached, $controller->teapotHeaderStatus());
+
+    // Use drupal_process_attached() to attach all the #attached stuff.
+    drupal_process_attached($attached);
+
+    // Return some arbitrary markup so the block doesn't disappear.
+    return ['#markup' => 'Headers handled by drupal_process_attached().'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
+}
