diff --git a/core/authorize.php b/core/authorize.php
index fd9e2f4..9b64112 100644
--- a/core/authorize.php
+++ b/core/authorize.php
@@ -21,6 +21,7 @@
  */
 
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 
 // Change the directory to the Drupal root.
 chdir('..');
@@ -40,7 +41,6 @@
  * Renders a 403 access denied page for authorize.php.
  */
 function authorize_access_denied_page() {
-  drupal_add_http_header('Status', '403 Forbidden');
   watchdog('access denied', 'authorize.php', NULL, WATCHDOG_WARNING);
   drupal_set_title('Access denied');
   return t('You are not allowed to access this page.');
@@ -90,6 +90,7 @@ function authorize_access_allowed() {
 $output = '';
 $show_messages = TRUE;
 
+$response = new Response();
 if (authorize_access_allowed()) {
   // Load both the Form API and Batch API.
   require_once __DIR__ . '/includes/form.inc';
@@ -153,9 +154,11 @@ function authorize_access_allowed() {
 }
 else {
   $output = authorize_access_denied_page();
+  $response->setStatusCode(403);
 }
 
 if (!empty($output)) {
-  drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
-  print theme('maintenance_page', array('content' => $output, 'show_messages' => $show_messages));
+  $response->setContent(theme('maintenance_page', array('content' => $output, 'show_messages' => $show_messages)));
+  $response->headers->set('Content-Type', 'text/html; charset=utf-8');
+  $response->send();
 }
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index be8c695..54f8b70 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -881,175 +881,11 @@ function drupal_load($type, $name) {
 }
 
 /**
- * Sets an HTTP response header for the current page.
- *
- * Note: When sending a Content-Type header, always include a 'charset' type,
- * too. This is necessary to avoid security bugs (e.g. UTF-7 XSS).
- *
- * @param $name
- *   The HTTP header name, or the special 'Status' header name.
- * @param $value
- *   The HTTP header value; if equal to FALSE, the specified header is unset.
- *   If $name is 'Status', this is expected to be a status code followed by a
- *   reason phrase, e.g. "404 Not Found".
- * @param $append
- *   Whether to append the value to an existing header or to replace it.
- *
- * @deprecated Header handling is being shifted to a Symfony response object.
- */
-function drupal_add_http_header($name, $value, $append = FALSE) {
-  // The headers as name/value pairs.
-  $headers = &drupal_static('drupal_http_headers', array());
-
-  $name_lower = strtolower($name);
-  _drupal_set_preferred_header_name($name);
-
-  if ($value === FALSE) {
-    $headers[$name_lower] = FALSE;
-  }
-  elseif (isset($headers[$name_lower]) && $append) {
-    // Multiple headers with identical names may be combined using comma (RFC
-    // 2616, section 4.2).
-    $headers[$name_lower] .= ',' . $value;
-  }
-  else {
-    $headers[$name_lower] = $value;
-  }
-}
-
-/**
- * Gets the HTTP response headers for the current page.
- *
- * @param $name
- *   An HTTP header name. If omitted, all headers are returned as name/value
- *   pairs. If an array value is FALSE, the header has been unset.
- *
- * @return
- *   A string containing the header value, or FALSE if the header has been set,
- *   or NULL if the header has not been set.
- *
- * @deprecated Header handling is being shifted to a Symfony response object.
- */
-function drupal_get_http_header($name = NULL) {
-  $headers = &drupal_static('drupal_http_headers', array());
-  if (isset($name)) {
-    $name = strtolower($name);
-    return isset($headers[$name]) ? $headers[$name] : NULL;
-  }
-  else {
-    return $headers;
-  }
-}
-
-/**
- * Sets the preferred name for the HTTP header.
- *
- * Header names are case-insensitive, but for maximum compatibility they should
- * follow "common form" (see RFC 2616, section 4.2).
- *
- * @deprecated Header handling is being shifted to a Symfony response object.
- */
-function _drupal_set_preferred_header_name($name = NULL) {
-  static $header_names = array();
-
-  if (!isset($name)) {
-    return $header_names;
-  }
-  $header_names[strtolower($name)] = $name;
-}
-
-/**
- * Sends the HTTP response headers that were previously set, adding defaults.
- *
- * Headers are set in drupal_add_http_header(). Default headers are not set
- * if they have been replaced or unset using drupal_add_http_header().
- *
- * @param array $default_headers
- *   (optional) An array of headers as name/value pairs.
- * @param bool $only_default
- *   (optional) If TRUE and headers have already been sent, send only the
- *   specified headers.
- *
- * @deprecated Header handling is being shifted to a Symfony response object.
- */
-function drupal_send_headers($default_headers = array(), $only_default = FALSE) {
-  $headers_sent = &drupal_static(__FUNCTION__, FALSE);
-  $headers = drupal_get_http_header();
-  if ($only_default && $headers_sent) {
-    $headers = array();
-  }
-  $headers_sent = TRUE;
-
-  $header_names = _drupal_set_preferred_header_name();
-  foreach ($default_headers as $name => $value) {
-    $name_lower = strtolower($name);
-    if (!isset($headers[$name_lower])) {
-      $headers[$name_lower] = $value;
-      $header_names[$name_lower] = $name;
-    }
-  }
-  foreach ($headers as $name_lower => $value) {
-    if ($name_lower == 'status') {
-      header($_SERVER['SERVER_PROTOCOL'] . ' ' . $value);
-    }
-    // Skip headers that have been unset.
-    elseif ($value !== FALSE) {
-      header($header_names[$name_lower] . ': ' . $value);
-    }
-  }
-}
-
-/**
- * Sets HTTP headers in preparation for a page response.
- *
- * Authenticated users are always given a 'no-cache' header, and will fetch a
- * fresh page on every request. This prevents authenticated users from seeing
- * locally cached pages.
- *
- * Also give each page a unique ETag. This will force clients to include both
- * an If-Modified-Since header and an If-None-Match header when doing
- * conditional requests for the page (required by RFC 2616, section 13.3.4),
- * making the validation more robust. This is a workaround for a bug in Mozilla
- * Firefox that is triggered when Drupal's caching is enabled and the user
- * accesses Drupal via an HTTP proxy (see
- * https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an authenticated
- * user requests a page, and then logs out and requests the same page again,
- * Firefox may send a conditional request based on the page that was cached
- * locally when the user was logged in. If this page did not have an ETag
- * header, the request only contains an If-Modified-Since header. The date will
- * be recent, because with authenticated users the Last-Modified header always
- * refers to the time of the request. If the user accesses Drupal via a proxy
- * server, and the proxy already has a cached copy of the anonymous page with an
- * older Last-Modified date, the proxy may respond with 304 Not Modified, making
- * the client think that the anonymous and authenticated pageviews are
- * identical.
- *
- * @see drupal_page_set_cache()
- *
- * @deprecated Header handling is being shifted to a Symfony response object.
- */
-function drupal_page_header() {
-  $headers_sent = &drupal_static(__FUNCTION__, FALSE);
-  if ($headers_sent) {
-    return TRUE;
-  }
-  $headers_sent = TRUE;
-
-  $default_headers = array(
-    'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT',
-    'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME),
-    'Cache-Control' => 'no-cache, must-revalidate, post-check=0, pre-check=0',
-    'ETag' => '"' . REQUEST_TIME . '"',
-  );
-  drupal_send_headers($default_headers);
-}
-
-/**
  * Sets HTTP headers in preparation for a cached page response.
  *
  * The headers allow as much as possible in proxies and browsers without any
  * particular knowledge about the pages. Modules can override these headers
- * using drupal_add_http_header().
+ * using a Kernel::RESPONSE event.
  *
  * If the request is conditional (using If-Modified-Since and If-None-Match),
  * and the conditions match those currently in the cache, a 304 Not Modified
@@ -1064,14 +900,11 @@ function drupal_serve_page_from_cache(stdClass $cache, Response $response, Reque
   $page_compression = !empty($cache->data['page_compressed']) && extension_loaded('zlib');
   $return_compressed = $page_compression && $request->server->has('HTTP_ACCEPT_ENCODING') && strpos($request->server->get('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE;
 
-  // Get headers. Keys are lower-case.
-  $boot_headers = drupal_get_http_header();
-
   foreach ($cache->data['headers'] as $name => $value) {
     // In the case of a 304 response, certain headers must be sent, and the
     // remaining may not (see RFC 2616, section 10.3.5).
     $name_lower = strtolower($name);
-    if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($boot_headers[$name_lower])) {
+    if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary'))) {
       $response->headers->set($name, $value);
       unset($cache->data['headers'][$name]);
     }
@@ -1103,7 +936,6 @@ function drupal_serve_page_from_cache(stdClass $cache, Response $response, Reque
   // Send the remaining headers.
   foreach ($cache->data['headers'] as $name => $value) {
     $response->headers->set($name, $value);
-    drupal_add_http_header($name, $value);
   }
 
   $response->setLastModified(\DateTime::createFromFormat('U', $cache->created));
@@ -1876,7 +1708,7 @@ function _drupal_bootstrap_page_cache() {
       exit;
     }
     else {
-      drupal_add_http_header('X-Drupal-Cache', 'MISS');
+      $request->attributes->set('_page_cache_disabled', TRUE);
     }
   }
 }
diff --git a/core/includes/common.inc b/core/includes/common.inc
index b97cc5e..2d836ff 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2533,7 +2533,10 @@ function drupal_process_attached($elements, $dependency_check = FALSE) {
   // special handling.
   foreach ($elements['#attached'] as $callback => $options) {
     foreach ($elements['#attached'][$callback] as $args) {
-      call_user_func_array($callback, $args);
+      // @FIXME Figure out how to properly deal with the http header.
+      if ($callback != 'drupal_add_http_header') {
+        call_user_func_array($callback, $args);
+      }
     }
   }
 
@@ -3127,8 +3130,6 @@ function _drupal_bootstrap_full($skip = FALSE) {
  *   The response body.
  * @return
  *   The cached object or NULL if the page cache was not set.
- *
- * @see drupal_page_header()
  */
 function drupal_page_set_cache(Response $response, Request $request) {
   if (drupal_page_is_cacheable()) {
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 0a57f16..f4a0880 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -953,8 +953,6 @@ function install_full_redirect_url($install_state) {
  *   An array of information about the current installation state.
  */
 function install_display_output($output, $install_state) {
-  drupal_page_header();
-
   // Prevent install.php from being indexed when installed in a sub folder.
   // robots.txt rules are not read if the site is within domain.com/subfolder
   // resulting in /subfolder/install.php being found through search engines.
@@ -978,7 +976,16 @@ function install_display_output($output, $install_state) {
     $active_task = $install_state['installation_finished'] ? NULL : $install_state['active_task'];
     drupal_add_region_content('sidebar_first', theme('task_list', array('items' => install_tasks_to_display($install_state), 'active' => $active_task, 'variant' => 'install')));
   }
-  print theme('install_page', array('content' => $output));
+  $response = new Response();
+  $default_headers = array(
+    'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT',
+    'Last-Modified' => gmdate(DATE_RFC1123, REQUEST_TIME),
+    'Cache-Control' => 'no-cache, must-revalidate, post-check=0, pre-check=0',
+    'ETag' => '"' . REQUEST_TIME . '"',
+  );
+  $response->headers->add($default_headers);
+  $response->setContent(theme('install_page', array('content' => $output)));
+  $response->send();
   exit;
 }
 
diff --git a/core/includes/update.inc b/core/includes/update.inc
index f185036..a61e808 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -18,6 +18,7 @@
 use Drupal\Component\Uuid\Uuid;
 use Drupal\Component\Utility\NestedArray;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 
 /**
  * Disables any extensions that are incompatible with the current core version.
@@ -154,12 +155,13 @@ function update_check_requirements($skip_warnings = FALSE) {
     );
     $status_report = drupal_render($status);
     $status_report .= 'Check the messages and <a href="' . check_url(drupal_requirements_url($severity)) . '">try again</a>.';
-    drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
     $maintenance_page = array(
       '#theme' => 'maintenance_page',
       '#content' => $status_report,
     );
-    print drupal_render($maintenance_page);
+    $response = new Response(drupal_render($maintenance_page));
+    $response->headers->set('Content-Type', 'text/html; charset=utf-8');
+    $response->send();
     exit();
   }
 }
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 8e4596f..0fe7df3 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -92,14 +92,6 @@ public function onRespond(FilterResponseEvent $event) {
     //   Response object does its own cache control processing and we intend to
     //   use partial page caching more extensively.
 
-    // Attach globally-declared headers to the response object so that Symfony
-    // can send them for us correctly.
-    // @todo remove this once we have removed all drupal_add_http_header() calls
-    $headers = drupal_get_http_header();
-    foreach ($headers as $name => $value) {
-      $response->headers->set($name, $value, FALSE);
-    }
-
     $max_age = \Drupal::config('system.performance')->get('cache.page.max_age');
     if ($max_age > 0 && ($cache = drupal_page_set_cache($response, $request))) {
       drupal_serve_page_from_cache($cache, $response, $request);
@@ -108,6 +100,12 @@ public function onRespond(FilterResponseEvent $event) {
       $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
       $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
     }
+
+    // If the page cache was marked as disabled/non active on the page set the
+    // header.
+    if ($request->attributes->get('_page_cache_disabled')) {
+      $response->headers->set('X-Drupal-Cache', 'MISS');
+    }
   }
 
   /**
diff --git a/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/EventSubscriber/ResponseSubscriber.php b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/EventSubscriber/ResponseSubscriber.php
new file mode 100644
index 0000000..165b967
--- /dev/null
+++ b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/EventSubscriber/ResponseSubscriber.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system_test\EventSubscriber\ResponseSubscriber.
+ */
+
+namespace Drupal\system_test\EventSubscriber;
+
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Sets the HTTP header coming in from the URL for a test.
+ *
+ * @see \Drupal\system\Tests\Bootstrap\PageCacheTest
+ */
+class ResponseSubscriber implements EventSubscriberInterface{
+
+  /**
+   * Sets the HTTP header coming in from the URL for a test.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    if ($event->getRequest()->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'system_test.set_header') {
+      $query = $event->getRequest()->query->all();
+      $event->getResponse()->headers->set($query['name'], $query['value']);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events = array();
+    $events[KernelEvents::RESPONSE] = 'onResponse';
+    return $events;
+  }
+
+} 
diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index 040d9f3..ca16129 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -9,7 +9,6 @@
  */
 function system_test_set_header() {
   $query = \Drupal::request()->query->all();
-  drupal_add_http_header($query['name'], $query['value']);
   return t('The following header was set: %name: %value', array('%name' => $query['name'], '%value' => $query['value']));
 }
 
diff --git a/core/modules/system/tests/modules/system_test/system_test.services.yml b/core/modules/system/tests/modules/system_test/system_test.services.yml
new file mode 100644
index 0000000..2af8b8b
--- /dev/null
+++ b/core/modules/system/tests/modules/system_test/system_test.services.yml
@@ -0,0 +1,5 @@
+services:
+  system_test.subscriber:
+    class: Drupal\system_test\EventSubscriber\ResponseSubscriber
+    tags:
+      - {name: event_subscriber}
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/Routing/ToolbarController.php b/core/modules/toolbar/lib/Drupal/toolbar/Routing/ToolbarController.php
index 3a7eb3c..7d7f82e 100644
--- a/core/modules/toolbar/lib/Drupal/toolbar/Routing/ToolbarController.php
+++ b/core/modules/toolbar/lib/Drupal/toolbar/Routing/ToolbarController.php
@@ -23,9 +23,9 @@ class ToolbarController extends ControllerBase {
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    */
   public function subtreesJsonp() {
-    _toolbar_initialize_page_cache();
+    $response = _toolbar_initialize_page_cache();
     $subtrees = toolbar_get_rendered_subtrees();
-    $response = new JsonResponse($subtrees);
+    $response = $response->setData($subtrees);
     $response->setCallback('Drupal.toolbar.setSubtrees.resolve');
     return $response;
   }
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index d91527b..381648b 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -9,6 +9,7 @@
 use Drupal\Core\Language\Language;
 use Drupal\Core\Template\Attribute;
 use Drupal\Component\Utility\Crypt;
+use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Response;
 use Drupal\menu_link\MenuLinkInterface;
 use Drupal\user\RoleInterface;
@@ -111,6 +112,9 @@ function toolbar_element_info() {
  *
  * @todo Replace this hack with something better integrated with DrupalKernel
  *   once Drupal's page caching itself is properly integrated.
+ *
+ * @return \Symfony\Component\HttpFoundation\JsonResponse
+ *   A JSON response object without the actual content.
  */
 function _toolbar_initialize_page_cache() {
   $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
@@ -134,15 +138,17 @@ function _toolbar_initialize_page_cache() {
   }
 
   // Otherwise, create a new page response (that will be cached).
-  drupal_add_http_header('X-Drupal-Cache', 'MISS');
+  $json_response = new JsonResponse();
+  $json_response->headers->set('X-Drupal-Cache', 'MISS');
 
   // The Expires HTTP header is the heart of the client-side HTTP caching. The
   // additional server-side page cache only takes effect when the client
   // accesses the callback URL again (e.g., after clearing the browser cache or
   // when force-reloading a Drupal page).
   $max_age = 3600 * 24 * 365;
-  drupal_add_http_header('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
-  drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
+  $json_response->headers->set('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
+  $json_response->headers->set('Cache-Control', 'private, max-age=' . $max_age);
+  return $json_response;
 }
 
 /**
diff --git a/core/update.php b/core/update.php
index 6491afc..d17b4de 100644
--- a/core/update.php
+++ b/core/update.php
@@ -222,8 +222,6 @@ function update_info_page() {
  *   Rendered HTML warning with 403 status.
  */
 function update_access_denied_page() {
-  drupal_add_http_header('Status', '403 Forbidden');
-  header(\Drupal::request()->server->get('SERVER_PROTOCOL') . ' 403 Forbidden');
   watchdog('access denied', 'update.php', NULL, WATCHDOG_WARNING);
   drupal_set_title('Access denied');
   return '<p>Access denied. You are not authorized to access this page. Log in using either an account with the <em>administer software updates</em> permission or the site maintenance account (the account you created during installation). If you cannot log in, you will have to edit <code>settings.php</code> to bypass this access check. To do this:</p>
@@ -383,6 +381,7 @@ function update_task_list($active = NULL) {
 ini_set('display_errors', TRUE);
 
 
+$response = new Response();
 // Only proceed with updates if the user is allowed to run them.
 if (update_access_allowed()) {
 
@@ -439,6 +438,7 @@ function update_task_list($active = NULL) {
 }
 else {
   $output = update_access_denied_page();
+  $response->setStatusCode(403);
 }
 if (isset($output) && $output) {
   // Explicitly start a session so that the update.php token will be accepted.
@@ -449,12 +449,14 @@ function update_task_list($active = NULL) {
     $output->send();
   }
   else {
-    drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
     $maintenance_page = array(
       '#theme' => 'maintenance_page',
       '#content' => $output,
       '#show_messages' => !$progress_page,
     );
-    print drupal_render($maintenance_page);
+    $response->setContent(drupal_render($maintenance_page));
+    $response->headers->set('Content-Type', 'text/html; charset=utf-8');
+    $response->send();
   }
+
 }
