diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 7dc75f5..4924ef3 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -1151,6 +1151,8 @@ function drupal_load($type, $name) {
  *   reason phrase, e.g. "404 Not Found".
  * @param $append
  *   Whether to append the value to an existing header or to replace it.
+ *
+ * @deprecated
  */
 function drupal_add_http_header($name, $value, $append = FALSE) {
   // The headers as name/value pairs.
@@ -1170,7 +1172,6 @@ function drupal_add_http_header($name, $value, $append = FALSE) {
   else {
     $headers[$name_lower] = $value;
   }
-  drupal_send_headers(array($name => $headers[$name_lower]), TRUE);
 }
 
 /**
@@ -1183,6 +1184,8 @@ function drupal_add_http_header($name, $value, $append = FALSE) {
  * @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
  */
 function drupal_get_http_header($name = NULL) {
   $headers = &drupal_static('drupal_http_headers', array());
@@ -1199,7 +1202,9 @@ function drupal_get_http_header($name = NULL) {
  * Sets the preferred name for the HTTP header.
  *
  * Header names are case-insensitive, but for maximum compatibility they should
- * follow "common form" (see RFC 2617, section 4.2).
+ * follow "common form" (see RFC 2616, section 4.2).
+ *
+ * @deprecated
  */
 function _drupal_set_preferred_header_name($name = NULL) {
   static $header_names = array();
@@ -1221,6 +1226,8 @@ function _drupal_set_preferred_header_name($name = NULL) {
  * @param bool $only_default
  *   (optional) If TRUE and headers have already been sent, send only the
  *   specified headers.
+ *
+ * @deprecated
  */
 function drupal_send_headers($default_headers = array(), $only_default = FALSE) {
   $headers_sent = &drupal_static(__FUNCTION__, FALSE);
@@ -1275,6 +1282,8 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE)
  * identical.
  *
  * @see drupal_page_set_cache()
+ *
+ * @deprecated
  */
 function drupal_page_header() {
   $headers_sent = &drupal_static(__FUNCTION__, FALSE);
@@ -1303,7 +1312,7 @@ function drupal_page_header() {
  * and the conditions match those currently in the cache, a 304 Not Modified
  * response is sent.
  */
-function drupal_serve_page_from_cache(stdClass $cache) {
+function drupal_serve_page_from_cache(stdClass $cache, \Symfony\Component\HttpFoundation\Response $response) {
   $config = config('system.performance');
 
   // Negotiate whether to use compression.
@@ -1322,7 +1331,7 @@ function drupal_serve_page_from_cache(stdClass $cache) {
     // 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])) {
-      drupal_add_http_header($name, $value);
+      $response->headers->set($name, $value);
       unset($cache->data['headers'][$name]);
     }
   }
@@ -1336,7 +1345,7 @@ function drupal_serve_page_from_cache(stdClass $cache) {
 
   // Entity tag should change if the output changes.
   $etag = '"' . $cache->created . '-' . intval($return_compressed) . '"';
-  header('Etag: ' . $etag);
+  $response->setEtag($etag);
 
   // See if the client has provided the required HTTP headers.
   $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE;
@@ -1345,25 +1354,23 @@ function drupal_serve_page_from_cache(stdClass $cache) {
   if ($if_modified_since && $if_none_match
       && $if_none_match == $etag // etag must match
       && $if_modified_since == $cache->created) {  // if-modified-since must match
-    header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
-    drupal_send_headers($default_headers);
+    $response->setStatusCode(304);
     return;
   }
 
   // Send the remaining headers.
   foreach ($cache->data['headers'] as $name => $value) {
+    $response->headers->set($name, $value);
     drupal_add_http_header($name, $value);
   }
 
-  $default_headers['Last-Modified'] = gmdate(DATE_RFC1123, $cache->created);
+  $response->setLastModified(\DateTime::createFromFormat('U', $cache->created));
 
   // HTTP/1.0 proxies does not support the Vary header, so prevent any caching
   // by sending an Expires date in the past. HTTP/1.1 clients ignores the
   // Expires header if a Cache-Control: max-age= directive is specified (see RFC
   // 2616, section 14.9.3).
-  $default_headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT';
-
-  drupal_send_headers($default_headers);
+  $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
 
   // Allow HTTP proxies to cache pages for anonymous users without a session
   // cookie. The Vary header is used to indicates the set of request-header
@@ -1371,17 +1378,17 @@ function drupal_serve_page_from_cache(stdClass $cache) {
   // response to reply to a subsequent request for a given URL without
   // revalidation.
   if (!isset($boot_headers['vary']) && !settings()->get('omit_vary_cookie')) {
-    header('Vary: Cookie');
+    $response->setVary('cookie');
   }
 
   if ($page_compression) {
-    header('Vary: Accept-Encoding', FALSE);
+    $response->setVary('accept-encoding', FALSE);
     // If page_compression is enabled, the cache contains gzipped data.
     if ($return_compressed) {
       // $cache->data['body'] is already gzip'ed, so make sure
       // zlib.output_compression does not compress it once more.
       ini_set('zlib.output_compression', '0');
-      header('Content-Encoding: gzip');
+      $response->headers->set('content-encoding', 'gzip');
     }
     else {
       // The client does not support compression, so unzip the data in the
@@ -1390,8 +1397,7 @@ function drupal_serve_page_from_cache(stdClass $cache) {
     }
   }
 
-  // Print the page.
-  print $cache->data['body'];
+  $response->setContent($cache->data['body']);
 }
 
 /**
@@ -2156,24 +2162,28 @@ function _drupal_bootstrap_page_cache() {
   // If there is no session cookie and cache is enabled (or forced), try
   // to serve a cached page.
   if (!isset($_COOKIE[session_name()]) && $cache_enabled) {
+    $response = new \Symfony\Component\HttpFoundation\Response();
     // Make sure there is a user object because its timestamp will be checked.
     $user = drupal_anonymous_user();
     // Get the page from the cache.
     $cache = drupal_page_get_cache();
     // If there is a cached page, display it.
     if (is_object($cache)) {
-      header('X-Drupal-Cache: HIT');
+      $response->headers->set('x-drupal-cache', 'HIT');
       // Restore the metadata cached with the page.
       _current_path($cache->data['path']);
       drupal_set_title($cache->data['title'], PASS_THROUGH);
       date_default_timezone_set(drupal_get_user_timezone());
 
-      drupal_serve_page_from_cache($cache);
+      drupal_serve_page_from_cache($cache, $response);
+
       // We are done.
+      $response->send();
       exit;
     }
     else {
-      header('X-Drupal-Cache: MISS');
+      $response->headers->set('x-drupal-cache', 'MISS');
+      $response->send();
     }
   }
 }
diff --git a/core/includes/common.inc b/core/includes/common.inc
index eabc080..6d7d5b6 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -4649,15 +4649,15 @@ function _drupal_bootstrap_full($skip = FALSE) {
  *
  * @see drupal_page_header()
  */
-function drupal_page_set_cache($body) {
+function drupal_page_set_cache(\Symfony\Component\HttpFoundation\Response $response, \Symfony\Component\HttpFoundation\Request $request) {
   global $base_root;
 
   if (drupal_page_is_cacheable()) {
     $cache = (object) array(
-      'cid' => $base_root . request_uri(),
+      'cid' => $base_root . $request->getRequestUri(),
       'data' => array(
-        'path' => current_path(),
-        'body' => $body,
+        'path' => $request->getQueryString(),
+        'body' => $response->getContent(),
         'title' => drupal_get_title(),
         'headers' => array(),
       ),
@@ -4666,17 +4666,12 @@ function drupal_page_set_cache($body) {
       'created' => REQUEST_TIME,
     );
 
-    // Restore preferred header names based on the lower-case names returned
-    // by drupal_get_http_header().
-    $header_names = _drupal_set_preferred_header_name();
-    foreach (drupal_get_http_header() as $name_lower => $value) {
-      $cache->data['headers'][$header_names[$name_lower]] = $value;
-      if ($name_lower == 'expires') {
-        // Use the actual timestamp from an Expires header if available.
-        $date = new DrupalDateTime($value);
-        $cache->expire = $date->getTimestamp();
-      }
-    }
+    // @todo just setting this on the response SHOULD be adequate.
+    $cache->data['headers'] = $response->headers->all();
+
+    // Use the actual timestamp from an Expires header if available.
+    $date = new DrupalDateTime($response->getExpires());
+    $cache->expire = $date->getTimestamp();
 
     if ($cache->data['body']) {
       if (config('system.performance')->get('response.gzip') && extension_loaded('zlib')) {
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index c5c491c..a01a0f4 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -46,20 +46,22 @@ public function onRespond(FilterResponseEvent $event) {
       return;
     }
 
+    $request = $event->getRequest();
     $response = $event->getResponse();
 
     // Set the X-UA-Compatible HTTP header to force IE to use the most recent
     // rendering engine or use Chrome's frame rendering engine if available.
-    $response->headers->set('X-UA-Compatible', 'IE=edge,chrome=1', false);
+    $response->headers->set('x-ua-compatible', 'IE=edge,chrome=1', FALSE);
 
     // Set the Content-language header.
-    $response->headers->set('Content-language', $this->languageManager->getLanguage(LANGUAGE_TYPE_INTERFACE)->langcode);
+    $response->headers->set('content-language', $this->languageManager->getLanguage(LANGUAGE_TYPE_INTERFACE)->langcode);
 
     // Because pages are highly dynamic, set the last-modified time to now
     // since the page is in fact being regenerated right now.
     // @todo Remove this and use a more intelligent default so that HTTP
     // caching can function properly.
-    $response->headers->set('Last-Modified', gmdate(DATE_RFC1123, REQUEST_TIME));
+    // @todo use $response->setLastModified()
+    $response->headers->set('last-modified', gmdate(DATE_RFC1123, REQUEST_TIME));
 
     // 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
@@ -81,26 +83,32 @@ public function onRespond(FilterResponseEvent $event) {
     // identical.
     // @todo Remove this line as no longer necessary per
     //   http://drupal.org/node/1573064
-    $response->headers->set('ETag', '"' . REQUEST_TIME . '"');
+    $response->setEtag(REQUEST_TIME);
 
     // 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.
     // @todo Revisit whether or not this is still appropriate now that the
-    //   Response object does its own cache control procesisng and we intend to
+    //   Response object does its own cache control processing and we intend to
     //   use partial page caching more extensively.
     // Commit the user session, if needed.
     drupal_session_commit();
+
+    // 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 = config('system.performance')->get('cache.page.max_age');
-    if ($max_age > 0 && ($cache = drupal_page_set_cache($response->getContent()))) {
-      drupal_serve_page_from_cache($cache);
-      // drupal_serve_page_from_cache() already printed the response.
-      $response->setContent('');
-      $response->headers->remove('cache-control');
+    if ($max_age > 0 && ($cache = drupal_page_set_cache($response, $request))) {
+      drupal_serve_page_from_cache($cache, $response);
     }
     else {
       $response->headers->set('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
-      $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
+      $response->headers->set('cache-control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
     }
   }
 
