 core/modules/aggregator/aggregator.routing.yml     |  3 ++
 .../EventSubscriber/DynamicPageCacheSubscriber.php | 45 +++++++++++++++++++++-
 .../src/Tests/DynamicPageCacheIntegrationTest.php  |  6 +++
 .../dynamic_page_cache_test.routing.yml            |  7 ++++
 .../src/DynamicPageCacheTestController.php         | 14 ++++++-
 core/modules/filter/filter.routing.yml             |  3 ++
 .../system/src/Controller/Http4xxController.php    |  3 ++
 .../src/Controller/AjaxTestController.php          |  3 ++
 .../src/Controller/EntityTestController.php        |  9 ++++-
 .../modules/test_page_test/src/Controller/Test.php |  3 ++
 .../src/Controller/TestPageTestController.php      |  1 +
 11 files changed, 92 insertions(+), 5 deletions(-)

diff --git a/core/modules/aggregator/aggregator.routing.yml b/core/modules/aggregator/aggregator.routing.yml
index 2f2d553..508a5de 100644
--- a/core/modules/aggregator/aggregator.routing.yml
+++ b/core/modules/aggregator/aggregator.routing.yml
@@ -84,3 +84,6 @@ aggregator.page_last:
     _title: 'Feed aggregator'
   requirements:
     _permission: 'access news feeds'
+  options:
+    # @todo Add cacheability metadata to this response.
+    no_cache: TRUE
diff --git a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
index 103b303..ec0b716 100644
--- a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
+++ b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\PageCache\RequestPolicyInterface;
 use Drupal\Core\PageCache\ResponsePolicyInterface;
 use Drupal\Core\Render\Element;
@@ -17,6 +18,7 @@
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 
 /**
@@ -44,11 +46,19 @@ class DynamicPageCacheSubscriber implements EventSubscriberInterface {
    * Attribute name of the Dynamic Page Cache request policy result.
    *
    * @see onRouteMatch()
-   * @see onRespond()
+   * @see onResponse()
    */
   const ATTRIBUTE_REQUEST_POLICY_RESULT = '_dynamic_page_cache_request_policy_result';
 
   /**
+   * Attribute name of the controller's render array #cache check.
+   *
+   * @see onViewRenderArray()
+   * @see onResponse()
+   */
+  const ATTRIBUTE_CONTROLLER_RESULT_RENDER_ARRAY_HAS_CACHEABILITY = '_dynamic_page_cache_controller_render_array_has_cacheability';
+
+  /**
    * Name of Dynamic Page Cache's response header.
    */
   const HEADER = 'X-Drupal-Dynamic-Cache';
@@ -149,10 +159,32 @@ public function onRouteMatch(GetResponseEvent $event) {
   }
 
   /**
+   * Complain if a controller returns a render array without #cache.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
+   *   The event to process.
+   *
+   * @see ::onResponse()
+   */
+  public function onViewRenderArray(GetResponseForControllerResultEvent $event) {
+    $request = $event->getRequest();
+    $result = $event->getControllerResult();
+
+    // Check if MainContentViewSubscriber would render this render array, i.e.
+    // if this render array will in fact be converted into a response.
+    if (is_array($result) && ($request->query->has(MainContentViewSubscriber::WRAPPER_FORMAT) || $request->getRequestFormat() == 'html')) {
+      // If #cache is not set, complain later.
+      $request->attributes->set(self::ATTRIBUTE_CONTROLLER_RESULT_RENDER_ARRAY_HAS_CACHEABILITY, isset($result['#cache']));
+    }
+  }
+
+  /**
    * Stores a response in case of a Dynamic Page Cache miss, if cacheable.
    *
    * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
    *   The event to process.
+   *
+   * @throws \LogicException
    */
   public function onResponse(FilterResponseEvent $event) {
     $response = $event->getResponse();
@@ -200,6 +232,13 @@ public function onResponse(FilterResponseEvent $event) {
       return;
     }
 
+    // If #cache was not set, complain.
+    // @see ::onViewRenderArray()
+    if ($request->attributes->get(self::ATTRIBUTE_CONTROLLER_RESULT_RENDER_ARRAY_HAS_CACHEABILITY) === FALSE) {
+      trigger_error('The controller for the route \'' . $request->attributes->get('_route') . '\' returned a render array without cacheability metadata. See https://www.drupal.org/developing/api/8/render/arrays/cacheability.', E_USER_ERROR);
+      return;
+    }
+
     // Embed the response object in a render array so that RenderCache is able
     // to cache it, handling cache redirection for us.
     $response_as_render_array = $this->responseToRenderArray($response);
@@ -323,6 +362,10 @@ public static function getSubscribedEvents() {
     // Run before HtmlResponseSubscriber::onRespond(), which has priority 0.
     $events[KernelEvents::RESPONSE][] = ['onResponse', 100];
 
+    // Run before MainContentViewSubscriber::onViewRenderArray(), which has
+    // priority 0.
+    $events[KernelEvents::VIEW][] = ['onViewRenderArray', 100];
+
     return $events;
   }
 
diff --git a/core/modules/dynamic_page_cache/src/Tests/DynamicPageCacheIntegrationTest.php b/core/modules/dynamic_page_cache/src/Tests/DynamicPageCacheIntegrationTest.php
index 5c174a8..31c1636 100644
--- a/core/modules/dynamic_page_cache/src/Tests/DynamicPageCacheIntegrationTest.php
+++ b/core/modules/dynamic_page_cache/src/Tests/DynamicPageCacheIntegrationTest.php
@@ -127,6 +127,12 @@ public function testDynamicPageCache() {
     // Cache.
     $this->drupalGet('dynamic-page-cache-test/html/uncacheable/tags');
     $this->assertEqual('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'By default, Drupal has no auto-placeholdering cache tags.');
+
+    // Finally, controllers returning render arrays without a top-level #cache
+    // trigger an exception urging the developer to add cacheability metadata.
+    $this->drupalGet(Url::fromRoute('dynamic_page_cache_test.html_without_cacheability'));
+    $this->assertResponse(200);
+    $this->assertRaw("The controller for the route 'dynamic_page_cache_test.html_without_cacheability' returned a render array without cacheability metadata. See https://www.drupal.org/developing/api/8/render/arrays/cacheability.");
   }
 
 }
diff --git a/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.routing.yml b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.routing.yml
index 77125b6..1b06b47 100644
--- a/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.routing.yml
+++ b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.routing.yml
@@ -73,3 +73,10 @@ dynamic_page_cache_test.html.uncacheable.tags:
     _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::htmlUncacheableTags'
   requirements:
     _access: 'TRUE'
+
+dynamic_page_cache_test.html_without_cacheability:
+  path: '/dynamic-page-cache-test/html-without-cacheability'
+  defaults:
+    _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::htmlWithoutCacheability'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/src/DynamicPageCacheTestController.php b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/src/DynamicPageCacheTestController.php
index b11984e..9550a45 100644
--- a/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/src/DynamicPageCacheTestController.php
+++ b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/src/DynamicPageCacheTestController.php
@@ -45,12 +45,12 @@ public function cacheableResponse() {
   }
 
   /**
-   * A route returning a render array (without cache contexts, so cacheable).
+   * A route returning a render array without #cache.
    *
    * @return array
    *   A render array.
    */
-  public function html() {
+  public function htmlWithoutCacheability() {
     return [
       'content' => [
         '#markup' => 'Hello world.',
@@ -59,6 +59,16 @@ public function html() {
   }
 
   /**
+   * A route returning a render array (without cache contexts, so cacheable).
+   *
+   * @return array
+   *   A render array.
+   */
+  public function html() {
+    return $this->htmlWithoutCacheability() + ['#cache' => []];
+  }
+
+  /**
    * A route returning a render array (with cache contexts, so cacheable).
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
diff --git a/core/modules/filter/filter.routing.yml b/core/modules/filter/filter.routing.yml
index 5b56da7..0724195 100644
--- a/core/modules/filter/filter.routing.yml
+++ b/core/modules/filter/filter.routing.yml
@@ -13,6 +13,9 @@ filter.tips:
     _title: 'Compose tips'
   requirements:
     _entity_access: 'filter_format.use'
+  options:
+    # @todo Add cacheability metadata to this response.
+    no_cache: TRUE
 
 filter.admin_overview:
   path: '/admin/config/content/formats'
diff --git a/core/modules/system/src/Controller/Http4xxController.php b/core/modules/system/src/Controller/Http4xxController.php
index c664324..ea69a00 100644
--- a/core/modules/system/src/Controller/Http4xxController.php
+++ b/core/modules/system/src/Controller/Http4xxController.php
@@ -23,6 +23,7 @@ class Http4xxController extends ControllerBase {
   public function on401() {
     return [
       '#markup' => $this->t('Please log in to access this page.'),
+      '#cache' => [],
     ];
   }
 
@@ -35,6 +36,7 @@ public function on401() {
   public function on403() {
     return [
       '#markup' => $this->t('You are not authorized to access this page.'),
+      '#cache' => [],
     ];
   }
 
@@ -47,6 +49,7 @@ public function on403() {
   public function on404() {
     return [
       '#markup' => $this->t('The requested page could not be found.'),
+      '#cache' => [],
     ];
   }
 
diff --git a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
index ff4859d..7501914 100644
--- a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
+++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
@@ -28,6 +28,7 @@ class AjaxTestController {
   public static function dialogContents() {
     // This is a regular render array; the keys do not have special meaning.
     $content = array(
+      '#cache' => [],
       '#title' => 'AJAX Dialog contents',
       'content' => array(
         '#markup' => 'Example message',
@@ -56,6 +57,7 @@ public static function dialogContents() {
    */
   public function render() {
     return [
+      '#cache' => [],
       '#attached' => [
         'library' => [
           'core/drupalSettings',
@@ -106,6 +108,7 @@ public function renderError(Request $request) {
    * Returns a render array of form elements and links for dialog.
    */
   public function dialog() {
+    $build['#cache'] = [];
     // Add two wrapper elements for testing non-modal dialogs. Modal dialogs use
     // the global drupal-modal wrapper by default.
     $build['dialog_wrappers'] = array('#markup' => '<div id="ajax-test-dialog-wrapper-1"></div><div id="ajax-test-dialog-wrapper-2"></div>');
diff --git a/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php b/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
index 1d6e4e8..33eb14b 100644
--- a/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
+++ b/core/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php
@@ -105,12 +105,16 @@ public function testAdmin() {
    *   A renderable array.
    */
   public function listReferencingEntities($entity_reference_field_name, $referenced_entity_type, $referenced_entity_id) {
+    $build = [
+      '#cache' => [],
+    ];
+
     // Early return if the referenced entity does not exist (or is deleted).
     $referenced_entity = $this->entityManager()
       ->getStorage($referenced_entity_type)
       ->load($referenced_entity_id);
     if ($referenced_entity === NULL) {
-      return array();
+      return $build;
     }
 
     $query = $this->entityQueryFactory
@@ -119,9 +123,10 @@ public function listReferencingEntities($entity_reference_field_name, $reference
     $entities = $this->entityManager()
       ->getStorage('entity_test')
       ->loadMultiple($query->execute());
-    return $this->entityManager()
+    $build += $this->entityManager()
       ->getViewBuilder('entity_test')
       ->viewMultiple($entities, 'full');
+    return $build;
   }
 
   /**
diff --git a/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php b/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php
index 934e6a2..3b330ec 100644
--- a/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php
+++ b/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php
@@ -24,6 +24,7 @@ public function renderTitle() {
     $build = array();
     $build['#markup'] = 'Hello Drupal';
     $build['#title'] = 'Foo';
+    $build['#cache'] = [];
 
     return $build;
   }
@@ -37,6 +38,7 @@ public function renderTitle() {
   public function staticTitle() {
     $build = array();
     $build['#markup'] = 'Hello Drupal';
+    $build['#cache'] = [];
 
     return $build;
   }
@@ -74,6 +76,7 @@ public function controllerWithCache() {
   public function renderPage() {
     return array(
       '#markup' => 'Content',
+      '#cache' => [],
     );
   }
 
diff --git a/core/modules/system/tests/modules/test_page_test/src/Controller/TestPageTestController.php b/core/modules/system/tests/modules/test_page_test/src/Controller/TestPageTestController.php
index ca423c8..552dab8 100644
--- a/core/modules/system/tests/modules/test_page_test/src/Controller/TestPageTestController.php
+++ b/core/modules/system/tests/modules/test_page_test/src/Controller/TestPageTestController.php
@@ -24,6 +24,7 @@ public function testPage() {
           'test-setting' => 'azAZ09();.,\\\/-_{}',
         ],
       ],
+      '#cache' => [],
     ];
   }
 
