 core/core.services.yml                             |   2 +-
 core/includes/bootstrap.inc                        | 170 +++++++-----------
 core/includes/common.inc                           |  67 +++----
 .../EventSubscriber/FinishResponseSubscriber.php   | 199 ++++++++++++++++-----
 .../system/Tests/Bootstrap/PageCacheTest.php       |  47 ++++-
 core/modules/toolbar/toolbar.module                |  11 +-
 6 files changed, 289 insertions(+), 207 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 9e2e188..fb2fbfa 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -578,7 +578,7 @@ services:
     class: Drupal\Core\EventSubscriber\FinishResponseSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@language_manager']
+    arguments: ['@language_manager', '@config.factory']
     scope: request
   redirect_response_subscriber:
     class: Drupal\Core\EventSubscriber\RedirectResponseSubscriber
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index fe34fc5..e5f980e 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -647,26 +647,27 @@ function drupal_page_cache_get_cid(Request $request) {
 /**
  * Retrieves the current page from the cache.
  *
- * Note: we do not serve cached pages to authenticated users, or to anonymous
- * users when $_SESSION is non-empty. $_SESSION may contain status messages
- * from a form submission, the contents of a shopping cart, or other user-
- * specific content that should not be cached and displayed to other users.
- *
  * @param \Symfony\Component\HttpFoundation\Request $request
  *   The request for this page.
  *
- * @return
- *   The cache object, if the page was found in the cache, NULL otherwise.
+ * @return \Symfony\Component\HttpFoundation\Response
+ *   The response, if the page was found in the cache, NULL otherwise.
  */
 function drupal_page_get_cache(Request $request) {
-  if (drupal_page_is_cacheable()) {
-    return \Drupal::cache('render')->get(drupal_page_cache_get_cid($request));
+  $cache = \Drupal::cache('render')->get(drupal_page_cache_get_cid($request));
+  if ($cache) {
+    return $cache->data;
   }
 }
 
 /**
  * Determines the cacheability of the current page.
  *
+ * Note: we do not serve cached pages to authenticated users, or to anonymous
+ * users when $_SESSION is non-empty. $_SESSION may contain status messages
+ * from a form submission, the contents of a shopping cart, or other user-
+ * specific content that should not be cached and displayed to other users.
+ *
  * @param $allow_caching
  *   Set to FALSE if you want to prevent this page to get cached.
  *
@@ -815,7 +816,7 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE)
  * 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
+ * Also give each page a unique ETag. This should 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
@@ -865,100 +866,62 @@ 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, Response $response, Request $request) {
-  $config = \Drupal::config('system.performance');
-
-  // First half: we must determine if we should be returning a 304.
-
-  // Negotiate whether to use compression.
-  $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])) {
-      $response->headers->set($name, $value);
-      unset($cache->data['headers'][$name]);
-    }
-  }
-
-  // If the client sent a session cookie, a cached copy will only be served
-  // to that one particular client due to Vary: Cookie. Thus, do not set
-  // max-age > 0, allowing the page to be cached by external proxies, when a
-  // session cookie is present unless the Vary header has been replaced.
-  $max_age = !$request->cookies->has(session_name()) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0;
-  // RFC 2616, section 14.21 says: 'To mark a response as "never expires," an
-  // origin server sends an Expires date approximately one year from the time
-  // the response is sent. HTTP/1.1 servers SHOULD NOT send Expires dates more
-  // than one year in the future.'
-  if ($max_age > 31536000 || $max_age === \Drupal\Core\Cache\Cache::PERMANENT) {
-    $max_age = 31536000;
-  }
-  $response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
-
-  // Entity tag should change if the output changes.
-  $response->setEtag($cache->created);
-
-  // See if the client has provided the required HTTP headers.
-  $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
-  $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
-
-  if ($if_modified_since && $if_none_match
-      && $if_none_match == $response->headers->get('etag') // etag must match
-      && $if_modified_since == $cache->created) {  // if-modified-since must match
-    $response->setStatusCode(304);
-    return;
-  }
-
-  // Second half: we're not returning a 304, so put in other headers.
-
-  // 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));
-
-  // 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).
-  if (!$response->getExpires()) {
-    $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
-  }
+function drupal_serve_page_from_cache(Response $response, Request $request) {
+  // Only allow caching in the browser and prevent that the response is stored
+  // by an external proxy server when the following conditions apply:
+  // 1. There is a session cookie on the request.
+  // 2. The Vary: Cookie header is on the response.
+  // 3. The Cache-Control header does not contain the no-cache directive.
+  if ($request->cookies->has(session_name()) &&
+    in_array('Cookie', $response->getVary()) &&
+    !$response->headers->hasCacheControlDirective('no-cache')) {
 
-  // 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
-  // fields that fully determines whether a cache is permitted to use the
-  // response to reply to a subsequent request for a given URL without
-  // revalidation.
-  if (!isset($boot_headers['vary']) && !Settings::get('omit_vary_cookie')) {
-    $response->setVary('Cookie', FALSE);
+    $response->setPrivate();
   }
 
-  if ($page_compression) {
-    $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
+  // Negotiate whether to use compression.
+  if ($response->headers->get('Content-Encoding') == 'gzip' && extension_loaded('zlib')) {
+    if (strpos($request->headers->get('Accept-Encoding'), 'gzip') !== FALSE) {
+      // The response content is already gzip'ed, so make sure
       // zlib.output_compression does not compress it once more.
       ini_set('zlib.output_compression', '0');
-      $response->headers->set('content-encoding', 'gzip');
     }
     else {
-      // The client does not support compression, so unzip the data in the
-      // cache. Strip the gzip header and run uncompress.
-      $cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
+      // The client does not support compression. Decompress the content and
+      // remove the Content-Encoding header.
+      $content = $response->getContent();
+      $content = gzinflate(substr(substr($content, 10), 0, -8));
+      $response->setContent($content);
+      $response->headers->remove('Content-Encoding');
     }
   }
 
-  $response->setContent($cache->data['body']);
+  // Perform HTTP revalidation.
+  $last_modified = $response->getLastModified();
+  if ($last_modified) {
+    // See if the client has provided the required HTTP headers.
+    $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
+    $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
+
+    $last_modified_match = $if_modified_since === $last_modified->getTimestamp();
+    $etag_match = $if_none_match == $response->getEtag();
+
+    if (($if_modified_since && $if_none_match && $last_modified_match && $etag_match) // If both revalidation request headers are present, apply both.
+        || ($if_modified_since && !$if_none_match && $last_modified_match) // Only Last-Modified must match.
+        || ($if_none_match && !$if_modified_since && $etag_match) // Only ETag must match.
+    ) {
+      $response->setStatusCode(304);
+      $response->setContent(NULL);
+
+      // In the case of a 304 response, certain headers must be sent, and the
+      // remaining may not (see RFC 2616, section 10.3.5).
+      foreach (array_keys($response->headers->all()) as $name) {
+        if (!in_array($name, array('etag', 'content-location', 'expires', 'cache-control', 'vary'))) {
+          $response->headers->remove($name);
+        }
+      }
+    }
+  }
 }
 
 /**
@@ -1624,8 +1587,6 @@ function _drupal_bootstrap_kernel() {
  * Attempts to serve a page from the cache.
  */
 function _drupal_bootstrap_page_cache() {
-  global $user;
-
   require_once __DIR__ . '/database.inc';
   // Check for a cache mode force from settings.php.
   if (Settings::get('page_cache_without_database')) {
@@ -1636,30 +1597,23 @@ function _drupal_bootstrap_page_cache() {
     $cache_enabled = $config->get('cache.page.use_internal');
   }
 
-  $request = Request::createFromGlobals();
+  $request = \Drupal::request();
   // If there is no session cookie and cache is enabled (or forced), try
   // to serve a cached page.
-  if (!$request->cookies->has(session_name()) && $cache_enabled) {
-    // Make sure there is a user object because its timestamp will be checked.
-    $user = new AnonymousUserSession();
+  if (!$request->cookies->has(session_name()) && $cache_enabled && drupal_page_is_cacheable()) {
     // Get the page from the cache.
-    $cache = drupal_page_get_cache($request);
+    $response = drupal_page_get_cache($request);
     // If there is a cached page, display it.
-    if (is_object($cache)) {
-      $response = new Response();
+    if ($response) {
       $response->headers->set('X-Drupal-Cache', 'HIT');
-      date_default_timezone_set(drupal_get_user_timezone());
 
-      drupal_serve_page_from_cache($cache, $response, $request);
+      drupal_serve_page_from_cache($response, $request);
 
       // We are done.
       $response->prepare($request);
       $response->send();
       exit;
     }
-    else {
-      drupal_add_http_header('X-Drupal-Cache', 'MISS');
-    }
   }
 }
 
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 0735dfa..0705365 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2873,55 +2873,42 @@ function _drupal_bootstrap_full($skip = FALSE) {
  * Page compression requires the PHP zlib extension
  * (http://php.net/manual/ref.zlib.php).
  *
- * @param $body
- *   The response body.
- * @return
- *   The cached object or NULL if the page cache was not set.
+ * @param \Symfony\Component\HttpFoundation\Response $response
+ *   The fully populated response.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ *   The request for this page.
  *
  * @see drupal_page_header()
  */
 function drupal_page_set_cache(Response $response, Request $request) {
-  if (drupal_page_is_cacheable()) {
-
-    // Check if the current page may be compressed.
-    $page_compressed = \Drupal::config('system.performance')->get('response.gzip') && extension_loaded('zlib');
-
-    $cache = (object) array(
-      'cid' => drupal_page_cache_get_cid($request),
-      'data' => array(
-        'body' => $response->getContent(),
-        'headers' => array(),
-        // We need to store whether page was compressed or not,
-        // because by the time it is read, the configuration might change.
-        'page_compressed' => $page_compressed,
-      ),
-      'tags' => HtmlViewSubscriber::convertHeaderToCacheTags($response->headers->get('X-Drupal-Cache-Tags')),
-      'expire' => Cache::PERMANENT,
-      'created' => REQUEST_TIME,
-    );
-
-    $cache->data['headers'] = $response->headers->all();
-
-    // Hack: exclude the x-drupal-cache header; it may make it in here because
-    // of awkwardness in how we defer sending it over in _drupal_page_get_cache.
-    if (isset($cache->data['headers']['x-drupal-cache'])) {
-      unset($cache->data['headers']['x-drupal-cache']);
+  // Check if the current page may be compressed.
+  if (\Drupal::config('system.performance')->get('response.gzip') &&
+    !$response->headers->get('Content-Encoding') && extension_loaded('zlib')) {
+
+    $content = $response->getContent();
+    if ($content) {
+      $response->setContent(gzencode($content, 9, FORCE_GZIP));
+      $response->headers->set('Content-Encoding', 'gzip');
     }
 
-     // Use the actual timestamp from an Expires header, if available.
-    if ($date = $response->getExpires()) {
-      $date = DrupalDateTime::createFromDateTime($date);
-      $cache->expire = $date->getTimestamp();
-    }
+    // When page compression is enabled, ensure that proxy caches will record
+    // and deliver different versions of a page depending on whether the
+    // client supports gzip or not.
+    $response->setVary('Accept-Encoding', FALSE);
+  }
 
-    if ($cache->data['body']) {
-      if ($page_compressed) {
-        $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP);
-      }
-      \Drupal::cache('render')->set($cache->cid, $cache->data, $cache->expire, $cache->tags);
+  // Use the actual timestamp from an Expires header, if available.
+  $expire = Cache::PERMANENT;
+  if ($response->headers->has('Expires')) {
+    $timestamp = $response->getExpires()->getTimestamp();
+    if ($timestamp > REQUEST_TIME) {
+      $expire = $timestamp;
     }
-    return $cache;
   }
+
+  $cid = drupal_page_cache_get_cid($request);
+  $tags = HtmlViewSubscriber::convertHeaderToCacheTags($response->headers->get('X-Drupal-Cache-Tags'));
+  \Drupal::cache('render')->set($cid, $response, $expire, $tags);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 8e4596f..a96b478 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -7,11 +7,16 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Site\Settings;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
-use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -27,13 +32,23 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
   protected $languageManager;
 
   /**
+   * A config object for the system performance configuration.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $config;
+
+  /**
    * Constructs a FinishResponseSubscriber object.
    *
    * @param LanguageManager $language_manager
    *  The LanguageManager object for retrieving the correct language code.
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   A config factory for retrieving required config objects.
    */
-  public function __construct(LanguageManager $language_manager) {
+  public function __construct(LanguageManager $language_manager, ConfigFactory $config_factory) {
     $this->languageManager = $language_manager;
+    $this->config = $config_factory->get('system.performance');
   }
 
   /**
@@ -57,57 +72,153 @@ public function onRespond(FilterResponseEvent $event) {
     // Set the Content-language header.
     $response->headers->set('Content-language', $this->languageManager->getCurrentLanguage()->id);
 
-    // 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->setLastModified(new \DateTime(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
-    // 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.
-    // @todo Remove this line as no longer necessary per
-    //   http://drupal.org/node/1573064
-    $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 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
+    // @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);
+    $is_cacheable = drupal_page_is_cacheable();
+
+    // Add headers necessary to specify whether the response should be cached by
+    // proxies and/or the browser.
+    if ($is_cacheable && $this->config->get('cache.page.max_age') > 0 && !$this->isCacheControlCustomized($response)) {
+      $this->setResponseCacheable($response, $request);
+    }
+    else {
+      $this->setResponseNotCacheable($response, $request);
+    }
+
+    // Store the response in the internal page cache.
+    if ($is_cacheable && $this->config->get('cache.page.use_internal')) {
+      drupal_page_set_cache($response, $request);
+      $response->headers->set('X-Drupal-Cache', 'MISS');
+      drupal_serve_page_from_cache($response, $request);
+    }
+  }
+
+  /**
+   * Determine whether the given response has a custom Cache-Control header.
+   *
+   * Upon construction, the ResponseHeaderBag is initialized with an empty
+   * Cache-Control header. Consequently it is not possible to check whether the
+   * header was set explicitly by simply checking its presence. Instead, it is
+   * necessary to examine the computed Cache-Control header and compare with
+   * values known to be present only when Cache-Control was never set
+   * explicitly.
+   *
+   * When neither Cache-Control nor any of the ETag, Last-Modified, Expires
+   * headers are set on the response, ::get('cache-control') returns the value
+   * 'no-cache'. If any of ETag, Last-Modified or Expires are set but not
+   * Cache-Control, then 'private, must-revalidate' (in exactly this order) is
+   * returned.
+   *
+   * @see \Symfony\Component\HttpFoundation\ResponseHeaderBag::computeCacheControlValue()
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *
+   * @return bool
+   *   TRUE when Cache-Control header was set explicitely on the given response.
+   */
+  protected function isCacheControlCustomized(Response $response) {
+    $cache_control = $response->headers->get('cache-control');
+    return $cache_control != 'no-cache' && $cache_control != 'private, must-revalidate';
+  }
+
+  /**
+   * Add Cache-Control and Expires headers to a response which is not cacheable.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   */
+  protected function setResponseNotCacheable(Response $response, Request $request) {
+    $this->setCacheControlNoCache($response);
+    $this->setExpiresNoCache($response);
+
+    // There is no point in sending along headers necessary for cache
+    // revalidation, if caching by proxies and browsers is denied in the first
+    // place. Therefore remove ETag, Last-Modified and Vary in that case.
+    $response->setEtag(NULL);
+    $response->setLastModified(NULL);
+    $response->setVary(NULL);
+  }
+
+  /**
+   * Add Cache-Control and Expires headers to a cacheable response.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   */
+  protected function setResponseCacheable(Response $response, Request $request) {
+    // HTTP/1.0 proxies do not support the Vary header, so prevent any caching
+    // by sending an Expires date in the past. HTTP/1.1 clients ignore the
+    // Expires header if a Cache-Control: max-age directive is specified (see
+    // RFC 2616, section 14.9.3).
+    if (!$response->headers->has('Expires')) {
+      $this->setExpiresNoCache($response);
+    }
+
+    $max_age = $this->config->get('cache.page.max_age');
+    // RFC 2616, section 14.21 says: 'To mark a response as "never expires," an
+    // origin server sends an Expires date approximately one year from the time
+    // the response is sent. HTTP/1.1 servers SHOULD NOT send Expires dates more
+    // than one year in the future.'
+    if ($max_age > 31536000 || $max_age === \Drupal\Core\Cache\Cache::PERMANENT) {
+      $max_age = 31536000;
+    }
+    $response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
+
+    // In order to support HTTP cache-revalidation, ensure that there is a
+    // Last-Modified and an ETag header on the response.
+    if (!$response->headers->has('Last-Modified')) {
+      $timestamp = REQUEST_TIME;
+      $response->setLastModified(new \DateTime(gmdate(DATE_RFC1123, REQUEST_TIME)));
     }
     else {
-      $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');
+      $timestamp = $response->getLastModified()->getTimestamp();
     }
+    $response->setEtag($timestamp);
+
+    // 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
+    // fields that fully determines whether a cache is permitted to use the
+    // response to reply to a subsequent request for a given URL without
+    // revalidation.
+    if (!$response->hasVary() && !Settings::get('omit_vary_cookie')) {
+      $response->setVary('Cookie', FALSE);
+    }
+  }
+
+  /**
+   * Disable caching in the browser and for HTTP/1.1 proxies and clients.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   */
+  protected function setCacheControlNoCache(Response $response) {
+    $response->headers->set('cache-control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
+  }
+
+  /**
+   * Disable caching in ancient browsers and for HTTP/1.0 proxies and clients.
+   *
+   * HTTP/1.0 proxies do not support the Vary header, so prevent any caching by
+   * sending an Expires date in the past. HTTP/1.1 clients ignore the Expires
+   * header if a Cache-Control: max-age= directive is specified (see RFC 2616,
+   * section 14.9.3).
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   */
+  protected function setExpiresNoCache(Response $response) {
+    $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
   }
 
   /**
@@ -116,7 +227,7 @@ public function onRespond(FilterResponseEvent $event) {
    * @return array
    *   An array of event listener definitions.
    */
-  static function getSubscribedEvents() {
+  public static function getSubscribedEvents() {
     $events[KernelEvents::RESPONSE][] = array('onRespond');
     return $events;
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
index 7f6b6a0..150e1db 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
@@ -128,21 +128,40 @@ function testConditionalRequests() {
 
     $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag));
     $this->assertResponse(304, 'Conditional request returned 304 Not Modified.');
+    $this->assertTrue(strlen($this->drupalGetContent()) == 0, 'No HTML returned.');
 
     $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC822, strtotime($last_modified)), 'If-None-Match: ' . $etag));
     $this->assertResponse(304, 'Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.');
+    $this->assertTrue(strlen($this->drupalGetContent()) == 0, 'No HTML returned.');
 
     $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC850, strtotime($last_modified)), 'If-None-Match: ' . $etag));
     $this->assertResponse(304, 'Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.');
+    $this->assertTrue(strlen($this->drupalGetContent()) == 0, 'No HTML returned.');
 
     $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified));
+    $this->assertResponse(304, 'Conditional request with If-Modified-Since but without If-None-Match returned 304 Not Modified.');
+    $this->assertTrue(strlen($this->drupalGetContent()) == 0, 'No HTML returned.');
+
+    $this->drupalGet('', array(), array('If-None-Match: ' . $etag));
+    $this->assertResponse(304, 'Conditional request with If-None-Match but without If-Modified-Since returned 304 Not Modified.');
+    $this->assertTrue(strlen($this->drupalGetContent()) == 0, 'No HTML returned.');
+
+    $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC1123, strtotime($last_modified) + 1), 'If-None-Match: ' . $etag));
+    $this->assertResponse(200, 'Conditional request with an If-Modified-Since date newer than Last-Modified returned 200 OK.');
     // Verify the page is not printed twice when the cache is warm.
     $this->assertNoPattern('#<html.*<html#');
-    $this->assertResponse(200, 'Conditional request without If-None-Match returned 200 OK.');
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
 
-    $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC1123, strtotime($last_modified) + 1), 'If-None-Match: ' . $etag));
-    $this->assertResponse(200, 'Conditional request with new a If-Modified-Since date newer than Last-Modified returned 200 OK.');
+    $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag . '-is-invalid'));
+    $this->assertResponse(200, 'Conditional request with an invalid If-None-Match ETag value returned 200 OK.');
+    // Verify the page is not printed twice when the cache is warm.
+    $this->assertNoPattern('#<html.*<html#');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
+
+    $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC1123, strtotime($last_modified) + 1), 'If-None-Match: ' . $etag . '-is-invalid'));
+    $this->assertResponse(200, 'Conditional request with an If-Modified-Since date nwer than Last-Modified and an invalid If-None-Match ETag value returned 200 OK.');
+    // Verify the page is not printed twice when the cache is warm.
+    $this->assertNoPattern('#<html.*<html#');
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
 
     $user = $this->drupalCreateUser();
@@ -196,15 +215,31 @@ function testPageCache() {
     $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent.');
     $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.');
     $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', 'Custom header was sent.');
+  }
+
+  /**
+   * Tests the omit_vary_cookie setting.
+   */
+  public function testPageCacheWithoutVaryCookie() {
+    $config = \Drupal::config('system.performance');
+    $config->set('cache.page.use_internal', 1);
+    $config->set('cache.page.max_age', 300);
+    $config->save();
 
-    // Check the omit_vary_cookie setting.
-    $this->drupalLogout();
     $settings['settings']['omit_vary_cookie'] = (object) array(
       'value' => TRUE,
       'required' => TRUE,
     );
     $this->writeSettings($settings);
-    $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar')));
+
+    // Fill the cache.
+    $this->drupalGet('');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
+    $this->assertTrue(strpos($this->drupalGetHeader('Vary'), 'Cookie') === FALSE, 'Vary: Cookie header was not sent.');
+
+    // Check cache.
+    $this->drupalGet('');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
     $this->assertTrue(strpos($this->drupalGetHeader('Vary'), 'Cookie') === FALSE, 'Vary: Cookie header was not sent.');
   }
 
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 0f58cb2..f1efa61 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -120,13 +120,11 @@ function _toolbar_initialize_page_cache() {
   // If we have a cache, serve it.
   // @see _drupal_bootstrap_page_cache()
   $request = \Drupal::request();
-  $cache = drupal_page_get_cache($request);
-  if (is_object($cache)) {
-    $response = new Response();
+  $response = drupal_page_get_cache($request);
+  if ($response) {
     $response->headers->set('X-Drupal-Cache', 'HIT');
-    date_default_timezone_set(drupal_get_user_timezone());
 
-    drupal_serve_page_from_cache($cache, $response, $request);
+    drupal_serve_page_from_cache($response, $request);
 
     $response->prepare($request);
     $response->send();
@@ -134,9 +132,6 @@ function _toolbar_initialize_page_cache() {
     exit;
   }
 
-  // Otherwise, create a new page response (that will be cached).
-  drupal_add_http_header('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
