diff --git a/core/modules/views/src/Plugin/views/display/Feed.php b/core/modules/views/src/Plugin/views/display/Feed.php
index 16d40813005a7bb57b53f7a8250137188aac8599..caf6f8cbc92f99c27edf3ba97e7d03f5f97108be 100644
--- a/core/modules/views/src/Plugin/views/display/Feed.php
+++ b/core/modules/views/src/Plugin/views/display/Feed.php
@@ -116,9 +116,47 @@ public static function buildResponse($view_id, $display_id, array $args = []) {
     $cache_metadata = CacheableMetadata::createFromRenderArray($build);
     $response->addCacheableDependency($cache_metadata);
 
+    // Set the HTTP headers and status code on the response if any bubbled.
+    if (!empty($build['#attached']['http_header'])) {
+      static::setHeaders($response, $build['#attached']['http_header']);
+    }
+
     return $response;
   }
 
+  /**
+   * Sets headers on a response object.
+   *
+   * @param \Drupal\Core\Cache\CacheableResponse $response
+   *   The HTML response to update.
+   * @param array $headers
+   *   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'.
+   *
+   * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor::setHeaders()
+   */
+  protected static function setHeaders(CacheableResponse $response, array $headers): void {
+    foreach ($headers as $values) {
+      $name = $values[0];
+      $value = $values[1];
+      $replace = !empty($values[2]);
+
+      // Drupal treats the HTTP response status code like a header, even though
+      // it really is not.
+      if (strtolower($name) === 'status') {
+        $response->setStatusCode($value);
+      }
+      else {
+        $response->headers->set($name, $value, $replace);
+      }
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/views/src/Plugin/views/style/Opml.php b/core/modules/views/src/Plugin/views/style/Opml.php
index ad8c30dfe6cba0192d28637b9daf0a6b301e9add..e59051dba68d433753ec24a139b8e752cec5022b 100644
--- a/core/modules/views/src/Plugin/views/style/Opml.php
+++ b/core/modules/views/src/Plugin/views/style/Opml.php
@@ -67,6 +67,11 @@ public function render() {
       '#view' => $this->view,
       '#options' => $this->options,
       '#rows' => $rows,
+      '#attached' => [
+        'http_header' => [
+          ['Content-Type', 'text/xml; charset=utf-8'],
+        ],
+      ],
     ];
     unset($this->view->row_index);
     return $build;
diff --git a/core/modules/views/src/Plugin/views/style/Rss.php b/core/modules/views/src/Plugin/views/style/Rss.php
index f8973550643f65df28878ddad1e5f17945342757..51394feda329fc29d3013b1d168c680aad8d7164 100644
--- a/core/modules/views/src/Plugin/views/style/Rss.php
+++ b/core/modules/views/src/Plugin/views/style/Rss.php
@@ -132,6 +132,11 @@ public function render() {
       '#view' => $this->view,
       '#options' => $this->options,
       '#rows' => $rows,
+      '#attached' => [
+        'http_header' => [
+          ['Content-Type', 'application/rss+xml; charset=utf-8'],
+        ],
+      ],
     ];
     unset($this->view->row_index);
     return $build;
diff --git a/core/modules/views/tests/src/Functional/Plugin/DisplayFeedTest.php b/core/modules/views/tests/src/Functional/Plugin/DisplayFeedTest.php
index f2510c690bf870d5c3f30119c0aa5d1427f59df7..6b6dd74019f2bdbe13a5f6b8a63e585b139d8048 100644
--- a/core/modules/views/tests/src/Functional/Plugin/DisplayFeedTest.php
+++ b/core/modules/views/tests/src/Functional/Plugin/DisplayFeedTest.php
@@ -204,4 +204,38 @@ public function testDisabledLinkedDisplay() {
     $this->assertSession()->statusCodeEquals(200);
   }
 
+  /**
+   * Tests the cacheability of the feed display.
+   */
+  public function testFeedCacheability(): void {
+    // Test as an anonymous user.
+    $this->drupalLogout();
+
+    // Set the page cache max age to a value greater than zero.
+    $config = $this->config('system.performance');
+    $config->set('cache.page.max_age', 300);
+    $config->save();
+
+    // Uninstall all page cache modules that could cache the HTTP response
+    // headers.
+    \Drupal::service('module_installer')->uninstall([
+      'page_cache',
+      'dynamic_page_cache',
+    ]);
+
+    // Reset all so that the config and module changes are active.
+    $this->resetAll();
+
+    $url = 'test-feed-display.xml';
+    $this->drupalGet($url);
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertSession()->responseHeaderEquals('Cache-Control', 'max-age=300, public');
+    $this->assertSession()->responseHeaderEquals('Content-Type', 'application/rss+xml; charset=utf-8');
+
+    // Visit the page again to get the cached response.
+    $this->drupalGet($url);
+    $this->assertSession()->responseHeaderEquals('Cache-Control', 'max-age=300, public');
+    $this->assertSession()->responseHeaderEquals('Content-Type', 'application/rss+xml; charset=utf-8');
+  }
+
 }
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index be6bfee2e4b9c9ec0c0a750259072cf9d565a22c..e048c714739028ce24a083a0f9d41a895c6095cb 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -928,12 +928,6 @@ function template_preprocess_views_view_rss(&$variables) {
   $variables['namespaces'] = new Attribute($style->namespaces);
   $variables['items'] = $items;
   $variables['channel_elements'] = $style->channel_elements;
-
-  // During live preview we don't want to output the header since the contents
-  // of the feed are being displayed inside a normal HTML page.
-  if (empty($variables['view']->live_preview)) {
-    $variables['view']->getResponse()->headers->set('Content-Type', 'application/rss+xml; charset=utf-8');
-  }
 }
 
 /**
@@ -995,12 +989,6 @@ function template_preprocess_views_view_opml(&$variables) {
   $variables['title'] = $title;
   $variables['items'] = $items;
   $variables['updated'] = gmdate(DATE_RFC2822, \Drupal::time()->getRequestTime());
-
-  // During live preview we don't want to output the header since the contents
-  // of the feed are being displayed inside a normal HTML page.
-  if (empty($variables['view']->live_preview)) {
-    $variables['view']->getResponse()->headers->set('Content-Type', 'text/xml; charset=utf-8');
-  }
 }
 
 /**