 .../EventSubscriber/DynamicPageCacheSubscriber.php | 45 ++++++++++++++++++++++
 core/modules/filter/filter.routing.yml             |  3 ++
 .../src/Controller/AjaxTestController.php          |  3 ++
 .../modules/test_page_test/src/Controller/Test.php |  3 ++
 .../src/Controller/TestPageTestController.php      |  1 +
 5 files changed, 55 insertions(+)

diff --git a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
index 103b303..35087b1 100644
--- a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
+++ b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
@@ -9,7 +9,9 @@
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponse;
 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 +19,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;
 
 /**
@@ -149,6 +152,44 @@ 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.
+   *
+   * @throws \LogicException
+   */
+  public function onViewRenderArray(GetResponseForControllerResultEvent $event) {
+    $request = $event->getRequest();
+    $result = $event->getControllerResult();
+
+    // Don't complain if Dynamic Page Cache's request subscriber did not fire,
+    // because that means it is impossible to have a Dynamic Page Cache hit,
+    // and hence complaining is unnecessary.
+    // @see ::onResponse()
+    if (!$request->attributes->has(self::ATTRIBUTE_REQUEST_POLICY_RESULT)) {
+      return;
+    }
+
+    // Don't complain if the Dynamic Page Cache request & response policies are
+    // not met, because that means Dynamic Page Cache won't cache this response
+    // and hence complaining is unnecessary.
+    // @see onRouteMatch()
+    if ($request->attributes->get(self::ATTRIBUTE_REQUEST_POLICY_RESULT) === RequestPolicyInterface::DENY || $this->responsePolicy->check(new CacheableResponse(), $request) === ResponsePolicyInterface::DENY) {
+      return;
+    }
+
+    // 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.
+      if (!isset($result['#cache'])) {
+        throw new \LogicException('The controller returned a render array without cacheability metadata. See https://www.drupal.org/developing/api/8/render/arrays/cacheability.');
+      }
+    }
+  }
+
+  /**
    * Stores a response in case of a Dynamic Page Cache miss, if cacheable.
    *
    * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
@@ -323,6 +364,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/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/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/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' => [],
     ];
   }
 
