.../Core/Render/StreamedResponseInterface.php | 17 +++++ core/modules/big_pipe/big_pipe.services.yml | 5 -- .../big_pipe/src/PageCache/DenyBigPipeResponse.php | 24 ------- .../page_cache/src/StackMiddleware/PageCache.php | 79 +++++++++++++++------- .../modules/page_cache/src/Tests/PageCacheTest.php | 28 +++++++- .../page_cache_form_test.info.yml | 0 .../page_cache_form_test.install | 0 .../page_cache_form_test.module | 0 .../page_cache_form_test.routing.yml | 0 .../src/Form/TestForm.php | 0 .../page_cache_test/page_cache_test.info.yml | 6 ++ .../page_cache_test/page_cache_test.routing.yml | 19 ++++++ .../src/PageCacheTestController.php | 31 +++++++++ .../src/PageCacheTestStreamedResponse.php | 25 +++++++ 14 files changed, 181 insertions(+), 53 deletions(-) diff --git a/core/lib/Drupal/Core/Render/StreamedResponseInterface.php b/core/lib/Drupal/Core/Render/StreamedResponseInterface.php new file mode 100644 index 0000000..524e1d5 --- /dev/null +++ b/core/lib/Drupal/Core/Render/StreamedResponseInterface.php @@ -0,0 +1,17 @@ +getStreamedResponse(); + if ($streamed_response !== FALSE) { + $this->storeResponse($request, $streamed_response); + } + } + } + /** * Sidesteps the page cache and directly forwards a request to the backend. * @@ -205,6 +220,43 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch /** @var \Symfony\Component\HttpFoundation\Response $response */ $response = $this->httpKernel->handle($request, $type, $catch); + // Currently it is not possible to cache binary file or streamed responses: + // https://github.com/symfony/symfony/issues/9128#issuecomment-25088678. + // Therefore exclude them, even for subclasses that implement + // CacheableResponseInterface. + if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) { + return $response; + } + // The exception: streamed responses that implement + // StreamedResponseInterface: those can be cached by PageCache during the + // 'terminate' phase/event. Already set the 'X-Drupal-Cache' header, so that + // header is present as expected. + // @see ::terminate() + elseif ($response instanceof StreamedResponseInterface) { + // Mark response as a cache miss. + $response->headers->set('X-Drupal-Cache', 'MISS'); + return $response; + } + + // Allow policy rules to further restrict which responses to cache. + if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { + return $response; + } + + $this->storeResponse($request, $response); + + return $response; + } + + /** + * Stores a response in the page cache. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * A request object. + * @param \Symfony\Component\HttpFoundation\Response $response + * A response object that should be stored in the page cache. + */ + protected function storeResponse(Request $request, Response $response) { // Drupal's primary cache invalidation architecture is cache tags: any // response that varies by a configuration value or data in a content // entity should have cache tags, to allow for instant cache invalidation @@ -227,27 +279,7 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch // so by replacing/extending this middleware service or adding another // one. if (!$response instanceof CacheableResponseInterface) { - return $response; - } - - // Mark response as a cache miss. - $response->headers->set('X-Drupal-Cache', 'MISS'); - - // Currently it is not possible to cache binary file or streamed responses: - // https://github.com/symfony/symfony/issues/9128#issuecomment-25088678. - // Therefore exclude them, even for subclasses that implement - // CacheableResponseInterface. - if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) { - return $response; - } - - // Allow policy rules to further restrict which responses to cache. - // @todo supporting BigPipe means that we return early here, which means - // that none of the code below runs. Thus: - // - page cache response policies won't work for BigPipe (which I guess can be okay since we need to duplicate that part of the page cache code in BigPipe anyway, because we need to assemble the final non-BigPipe response from it and then store it in Page Cache - // - for the same reason, we need to handle all the other logic below: 4xx responses cache tags + expiration date - if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { - return $response; + return; } // The response passes all of the above checks, so cache it. @@ -260,7 +292,8 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch $expire = ($date > time()) ? $date : Cache::PERMANENT; $this->set($request, $response, $expire, $tags); - return $response; + // Mark response as a cache miss. + $response->headers->set('X-Drupal-Cache', 'MISS'); } /** diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index 44c94e1..10a1770 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -24,7 +24,7 @@ class PageCacheTest extends WebTestBase { * * @var array */ - public static $modules = array('test_page_test', 'system_test', 'entity_test'); + public static $modules = array('test_page_test', 'system_test', 'entity_test', 'page_cache_test'); /** * {@inheritdoc} @@ -484,4 +484,30 @@ public function testCacheableResponseResponses() { $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Drupal page cache header not found.'); } + /** + * Tests Page Cache's support for streamed responses. + * + * @see \Drupal\page_cache\StackMiddleware\PageCache::terminate() + */ + public function testStreamedResponses() { + // Symfony streamed response: not even considered. + $this->drupalGet(Url::fromRoute('page_cache_test.symfony_streamed_response')); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache')); + $this->drupalGet(Url::fromRoute('page_cache_test.symfony_streamed_response')); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache')); + + // StreamedResponseInterface Response, with untracked stream: always cache + // miss. + $this->drupalGet(Url::fromRoute('page_cache_test.streamed_response_interface.untracked')); + $this->assertIdentical('MISS', $this->drupalGetHeader('X-Drupal-Cache')); + $this->drupalGet(Url::fromRoute('page_cache_test.streamed_response_interface.untracked')); + $this->assertIdentical('MISS', $this->drupalGetHeader('X-Drupal-Cache')); + + // StreamedResponseInterface Response, with tracked stream: cacheable. + $this->drupalGet(Url::fromRoute('page_cache_test.streamed_response_interface.tracked')); + $this->assertIdentical('MISS', $this->drupalGetHeader('X-Drupal-Cache')); + $this->drupalGet(Url::fromRoute('page_cache_test.streamed_response_interface.tracked')); + $this->assertIdentical('HIT', $this->drupalGetHeader('X-Drupal-Cache')); + } + } diff --git a/core/modules/page_cache/tests/modules/page_cache_form_test.info.yml b/core/modules/page_cache/tests/modules/page_cache_form_test/page_cache_form_test.info.yml similarity index 100% rename from core/modules/page_cache/tests/modules/page_cache_form_test.info.yml rename to core/modules/page_cache/tests/modules/page_cache_form_test/page_cache_form_test.info.yml diff --git a/core/modules/page_cache/tests/modules/page_cache_form_test.install b/core/modules/page_cache/tests/modules/page_cache_form_test/page_cache_form_test.install similarity index 100% rename from core/modules/page_cache/tests/modules/page_cache_form_test.install rename to core/modules/page_cache/tests/modules/page_cache_form_test/page_cache_form_test.install diff --git a/core/modules/page_cache/tests/modules/page_cache_form_test.module b/core/modules/page_cache/tests/modules/page_cache_form_test/page_cache_form_test.module similarity index 100% rename from core/modules/page_cache/tests/modules/page_cache_form_test.module rename to core/modules/page_cache/tests/modules/page_cache_form_test/page_cache_form_test.module diff --git a/core/modules/page_cache/tests/modules/page_cache_form_test.routing.yml b/core/modules/page_cache/tests/modules/page_cache_form_test/page_cache_form_test.routing.yml similarity index 100% rename from core/modules/page_cache/tests/modules/page_cache_form_test.routing.yml rename to core/modules/page_cache/tests/modules/page_cache_form_test/page_cache_form_test.routing.yml diff --git a/core/modules/page_cache/tests/modules/src/Form/TestForm.php b/core/modules/page_cache/tests/modules/page_cache_form_test/src/Form/TestForm.php similarity index 100% rename from core/modules/page_cache/tests/modules/src/Form/TestForm.php rename to core/modules/page_cache/tests/modules/page_cache_form_test/src/Form/TestForm.php diff --git a/core/modules/page_cache/tests/modules/page_cache_test/page_cache_test.info.yml b/core/modules/page_cache/tests/modules/page_cache_test/page_cache_test.info.yml new file mode 100644 index 0000000..bef43eb --- /dev/null +++ b/core/modules/page_cache/tests/modules/page_cache_test/page_cache_test.info.yml @@ -0,0 +1,6 @@ +name: 'Page Cache Test' +type: module +description: 'Support module for the Page Cache module tests.' +core: 8.x +package: Testing +version: VERSION diff --git a/core/modules/page_cache/tests/modules/page_cache_test/page_cache_test.routing.yml b/core/modules/page_cache/tests/modules/page_cache_test/page_cache_test.routing.yml new file mode 100644 index 0000000..5f72632 --- /dev/null +++ b/core/modules/page_cache/tests/modules/page_cache_test/page_cache_test.routing.yml @@ -0,0 +1,19 @@ +page_cache_test.symfony_streamed_response: + path: '/page_cache_test/symfony_streamedresponse' + defaults: + _controller: '\Drupal\page_cache_test\PageCacheTestController::symfonyStreamedResponse' + requirements: + _access: 'TRUE' + +page_cache_test.streamed_response_interface.untracked: + path: '/page_cache_test/streamed_response_interface/untracked' + defaults: + _controller: '\Drupal\page_cache_test\PageCacheTestController::streamedResponseInterfaceUntracked' + requirements: + _access: 'TRUE' +page_cache_test.streamed_response_interface.tracked: + path: '/page_cache_test/streamed_response_interface/tracked' + defaults: + _controller: '\Drupal\page_cache_test\PageCacheTestController::streamedResponseInterfaceTracked' + requirements: + _access: 'TRUE' diff --git a/core/modules/page_cache/tests/modules/page_cache_test/src/PageCacheTestController.php b/core/modules/page_cache/tests/modules/page_cache_test/src/PageCacheTestController.php new file mode 100644 index 0000000..17d014f --- /dev/null +++ b/core/modules/page_cache/tests/modules/page_cache_test/src/PageCacheTestController.php @@ -0,0 +1,31 @@ +setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 UTC'))); + } + +} diff --git a/core/modules/page_cache/tests/modules/page_cache_test/src/PageCacheTestStreamedResponse.php b/core/modules/page_cache/tests/modules/page_cache_test/src/PageCacheTestStreamedResponse.php new file mode 100644 index 0000000..5e20353 --- /dev/null +++ b/core/modules/page_cache/tests/modules/page_cache_test/src/PageCacheTestStreamedResponse.php @@ -0,0 +1,25 @@ +streamedResponse = $streamed_response; + parent::__construct('', 200, []); + } + + public function getStreamedResponse() { + return $this->streamedResponse; + } +}