From 19f80f658ed319cb41ffd78082e5582673aa1ef0 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Wed, 2 Oct 2024 13:39:07 -0700 Subject: [PATCH] Issue #2951814 by wim leers, mxr576, vakulrai, claudiu.cristea, neclimdul, alexpott, sanjayk, joachim, chi, smustgrave: Improve X-Drupal-Cache and X-Drupal-Dynamic-Cache headers, even for responses that are not cacheable --- .../tests/src/Functional/BasicAuthTest.php | 4 +- .../src/Functional/DbLogResourceTest.php | 3 +- .../DynamicPageCacheSubscriber.php | 16 +++- .../DynamicPageCacheIntegrationTest.php | 21 ++--- .../tests/src/Functional/FileUploadTest.php | 2 +- .../jsonapi/tests/src/Functional/NodeTest.php | 4 +- .../Functional/ResourceResponseTestTrait.php | 2 +- .../tests/src/Functional/ResourceTestBase.php | 89 +++++++++---------- ...uageBrowserDetectionAcceptLanguageTest.php | 12 +-- .../src/Functional/LayoutSectionTest.php | 11 +-- .../Functional/NodeBlockFunctionalTest.php | 2 +- .../src/StackMiddleware/PageCache.php | 16 +++- .../tests/src/Functional/PageCacheTest.php | 12 +-- .../EntityResource/EntityResourceTestBase.php | 17 ++-- .../tests/src/Functional/ResourceTestBase.php | 8 +- .../src/Functional/Session/SessionTest.php | 2 +- .../Functional/System/ErrorHandlerTest.php | 2 +- .../src/Functional/UserPasswordResetTest.php | 4 +- .../tests/src/Traits/StandardTestTrait.php | 2 +- 19 files changed, 127 insertions(+), 102 deletions(-) diff --git a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php index c91b28eabf96..2d3ede221525 100644 --- a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php +++ b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php @@ -53,7 +53,7 @@ public function testBasicAuth(): void { $this->assertSession()->pageTextContains($account->getAccountName()); $this->assertSession()->statusCodeEquals(200); $this->mink->resetSessions(); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); // Check that Cache-Control is not set to public. $this->assertSession()->responseHeaderNotContains('Cache-Control', 'public'); @@ -87,7 +87,7 @@ public function testBasicAuth(): void { $this->drupalGet($url); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS'); $this->basicAuthGet($url, $account->getAccountName(), $account->pass_raw); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); // Check that Cache-Control is not set to public. $this->assertSession()->responseHeaderNotContains('Cache-Control', 'public'); } diff --git a/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php b/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php index efd97a84aa90..b93c5e1b22a5 100644 --- a/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php +++ b/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php @@ -85,7 +85,8 @@ public function testWatchdog(): void { "The 'restful get dblog' permission is required.", $response, ['4xx-response', 'http_response'], - ['user.permissions'] + ['user.permissions'], + 'UNCACHEABLE (request policy)' ); // Create a user account that has the required permissions to read diff --git a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php index 005d6a194584..3cf49b65c516 100644 --- a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php +++ b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php @@ -156,11 +156,18 @@ public function onRequest(RequestEvent $event) { public function onResponse(ResponseEvent $event) { $response = $event->getResponse(); + // Don't indicate non-cacheability on responses to uncacheable requests. + // @see https://tools.ietf.org/html/rfc7231#section-4.2.3 + if (!$event->getRequest()->isMethodCacheable()) { + return; + } + // Dynamic Page Cache only works with cacheable responses. It does not work // with plain Response objects. (Dynamic Page Cache needs to be able to // access and modify the cacheability metadata associated with the // response.) if (!$response instanceof CacheableResponseInterface) { + $response->headers->set(self::HEADER, 'UNCACHEABLE (no cacheability)'); return; } @@ -172,7 +179,7 @@ public function onResponse(ResponseEvent $event) { // There's no work left to be done if this is an uncacheable response. if (!$this->shouldCacheResponse($response)) { // The response is uncacheable, mark it as such. - $response->headers->set(self::HEADER, 'UNCACHEABLE'); + $response->headers->set(self::HEADER, 'UNCACHEABLE (poor cacheability)'); return; } @@ -195,7 +202,12 @@ public function onResponse(ResponseEvent $event) { // Don't cache the response if the Dynamic Page Cache request & response // policies are not met. // @see onRequest() - if ($this->requestPolicyResults[$request] === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { + if ($this->requestPolicyResults[$request] === RequestPolicyInterface::DENY) { + $response->headers->set(self::HEADER, 'UNCACHEABLE (request policy)'); + return; + } + if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { + $response->headers->set(self::HEADER, 'UNCACHEABLE (response policy)'); return; } diff --git a/core/modules/dynamic_page_cache/tests/src/Functional/DynamicPageCacheIntegrationTest.php b/core/modules/dynamic_page_cache/tests/src/Functional/DynamicPageCacheIntegrationTest.php index afa08e131749..ac26aeab3a33 100644 --- a/core/modules/dynamic_page_cache/tests/src/Functional/DynamicPageCacheIntegrationTest.php +++ b/core/modules/dynamic_page_cache/tests/src/Functional/DynamicPageCacheIntegrationTest.php @@ -53,7 +53,7 @@ public function testDynamicPageCache(): void { // Cache. $url = Url::fromUri('route:dynamic_page_cache_test.response'); $this->drupalGet($url); - $this->assertSession()->responseHeaderDoesNotExist(DynamicPageCacheSubscriber::HEADER); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (no cacheability)'); // Controllers returning CacheableResponseInterface (cacheable response) // objects are handled by Dynamic Page Cache. @@ -97,27 +97,28 @@ public function testDynamicPageCache(): void { // response, are ignored by Dynamic Page Cache (but only because those // wrapper formats' responses do not implement CacheableResponseInterface). $this->drupalGet('dynamic-page-cache-test/html', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]); - $this->assertSession()->responseHeaderDoesNotExist(DynamicPageCacheSubscriber::HEADER); $this->drupalGet('dynamic-page-cache-test/html', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog']]); - $this->assertSession()->responseHeaderDoesNotExist(DynamicPageCacheSubscriber::HEADER); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (no cacheability)'); $this->drupalGet('dynamic-page-cache-test/html', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal']]); - $this->assertSession()->responseHeaderDoesNotExist(DynamicPageCacheSubscriber::HEADER); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (no cacheability)'); // Admin routes are ignored by Dynamic Page Cache. $this->drupalGet('dynamic-page-cache-test/html/admin'); - $this->assertSession()->responseHeaderDoesNotExist(DynamicPageCacheSubscriber::HEADER); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (response policy)'); $this->drupalGet('dynamic-page-cache-test/response/admin'); - $this->assertSession()->responseHeaderDoesNotExist(DynamicPageCacheSubscriber::HEADER); + // Even this is an admin route, the lack of cacheability prevails in the + // header explanation. + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (no cacheability)'); $this->drupalGet('dynamic-page-cache-test/cacheable-response/admin'); - $this->assertSession()->responseHeaderDoesNotExist(DynamicPageCacheSubscriber::HEADER); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (response policy)'); // Max-age = 0 responses are ignored by Dynamic Page Cache. $this->drupalGet('dynamic-page-cache-test/html/uncacheable/max-age'); - $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE'); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (poor cacheability)'); // 'user' cache context responses are ignored by Dynamic Page Cache. $this->drupalGet('dynamic-page-cache-test/html/uncacheable/contexts'); - $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE'); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (poor cacheability)'); // 'current-temperature' cache tag responses are ignored by Dynamic Page // Cache. @@ -127,7 +128,7 @@ public function testDynamicPageCache(): void { // Route access checkers can also bubble up cacheability data. $this->drupalGet('/dynamic-page-cache-test/html/uncacheable/route-access'); $this->assertSession()->responseHeaderExists(DynamicPageCacheSubscriber::HEADER); - $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE'); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (poor cacheability)'); } } diff --git a/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php b/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php index 9877b96cf383..a667ea6653bb 100644 --- a/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php +++ b/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php @@ -287,7 +287,7 @@ public function testPostFileUploadAndUseInSingleRequest(): void { // This request fails despite the upload succeeding, because we're not // allowed to view the entity we're uploading to. $response = $this->fileRequest($uri, $this->testFileData); - $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $uri, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site', 'user.permissions'], FALSE, 'UNCACHEABLE'); + $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $uri, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site', 'user.permissions']); $this->setUpAuthorization('GET'); diff --git a/core/modules/jsonapi/tests/src/Functional/NodeTest.php b/core/modules/jsonapi/tests/src/Functional/NodeTest.php index 0150d1376ab5..f0d490976523 100644 --- a/core/modules/jsonapi/tests/src/Functional/NodeTest.php +++ b/core/modules/jsonapi/tests/src/Functional/NodeTest.php @@ -345,7 +345,7 @@ public function testGetIndividual(): void { '/data', ['4xx-response', 'http_response', 'node:1'], ['url.query_args', 'url.site', 'user.permissions'], - FALSE, + 'UNCACHEABLE (request policy)', 'MISS' ); @@ -356,7 +356,7 @@ public function testGetIndividual(): void { // context to be optimized away. $expected_cache_contexts = Cache::mergeContexts($this->getExpectedCacheContexts(), ['user']); $expected_cache_contexts = array_diff($expected_cache_contexts, ['user.permissions']); - $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $expected_cache_contexts, FALSE, 'UNCACHEABLE'); + $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $expected_cache_contexts, 'UNCACHEABLE (request policy)', 'UNCACHEABLE (poor cacheability)'); } /** diff --git a/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php b/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php index 510035a070ab..07b63ded251d 100644 --- a/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php +++ b/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php @@ -244,7 +244,7 @@ protected static function toResourceResponse(ResponseInterface $response) { $cacheability->addCacheContexts(explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0])); } if ($dynamic_cache = $response->getHeader('X-Drupal-Dynamic-Cache')) { - $cacheability->setCacheMaxAge(($dynamic_cache[0] === 'UNCACHEABLE' && $response->getStatusCode() < 400) ? 0 : Cache::PERMANENT); + $cacheability->setCacheMaxAge((str_contains($dynamic_cache[0], 'UNCACHEABLE') && $response->getStatusCode() < 400) ? 0 : Cache::PERMANENT); } $related_document = Json::decode($response->getBody()); $resource_response = new CacheableResourceResponse($related_document, $response->getStatusCode()); diff --git a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php index c37f77ee6b4c..2fe7a4510db7 100644 --- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php @@ -749,17 +749,16 @@ protected function assertResourceResponse($expected_status_code, $expected_docum $this->assertTrue($response->hasHeader('X-Drupal-Cache')); $this->assertSame($expected_page_cache_header_value, $response->getHeader('X-Drupal-Cache')[0]); } - else { - $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + elseif ($response->hasHeader('X-Drupal-Cache')) { + $this->assertMatchesRegularExpression('#^UNCACHEABLE \((no cacheability|(request|response) policy)\)$#', $response->getHeader('X-Drupal-Cache')[0]); } - // Expected Dynamic Page Cache header value: X-Drupal-Dynamic-Cache header. if ($expected_dynamic_page_cache_header_value !== FALSE) { $this->assertTrue($response->hasHeader('X-Drupal-Dynamic-Cache')); $this->assertSame($expected_dynamic_page_cache_header_value, $response->getHeader('X-Drupal-Dynamic-Cache')[0]); } - else { - $this->assertFalse($response->hasHeader('X-Drupal-Dynamic-Cache')); + elseif ($response->hasHeader('X-Drupal-Dynamic-Cache')) { + $this->assertMatchesRegularExpression('#^UNCACHEABLE \(((no|poor) cacheability|(request|response) policy)\)$#', $response->getHeader('X-Drupal-Dynamic-Cache')[0]); } } @@ -935,8 +934,8 @@ public function testGetIndividual(): void { $reason = $this->getExpectedUnauthorizedAccessMessage('GET'); $message = trim("The current user is not allowed to GET the selected resource. $reason"); // MISS or UNCACHEABLE depends on data. It must not be HIT. - $dynamic_cache_header_value = !empty(array_intersect(['user', 'session'], $expected_403_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; - $this->assertResourceErrorResponse(403, $message, $url, $response, '/data', $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), FALSE, $dynamic_cache_header_value); + $dynamic_cache_header_value = !empty(array_intersect(['user', 'session'], $expected_403_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; + $this->assertResourceErrorResponse(403, $message, $url, $response, '/data', $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache_header_value); $this->assertArrayNotHasKey('Link', $response->getHeaders()); } else { @@ -945,8 +944,8 @@ public function testGetIndividual(): void { $expected_document['data']['attributes'] = array_intersect_key($expected_document['data']['attributes'], [$label_field_name => TRUE]); unset($expected_document['data']['relationships']); // MISS or UNCACHEABLE depends on data. It must not be HIT. - $dynamic_cache_label_only = !empty(array_intersect(['user', 'session'], $this->getExpectedCacheContexts([$label_field_name]))) ? 'UNCACHEABLE' : 'MISS'; - $this->assertResourceResponse(200, $expected_document, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts([$label_field_name]), FALSE, $dynamic_cache_label_only); + $dynamic_cache_label_only = !empty(array_intersect(['user', 'session'], $this->getExpectedCacheContexts([$label_field_name]))) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; + $this->assertResourceResponse(200, $expected_document, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts([$label_field_name]), 'UNCACHEABLE (request policy)', $dynamic_cache_label_only); } $this->setUpAuthorization('GET'); @@ -972,20 +971,20 @@ public function testGetIndividual(): void { ], ], ]; - $this->assertResourceResponse(400, $expected_document, $response, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], FALSE, 'MISS'); + $this->assertResourceResponse(400, $expected_document, $response, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], 'UNCACHEABLE (request policy)', 'MISS'); // 200 for well-formed HEAD request. $response = $this->request('HEAD', $url, $request_options); // MISS or UNCACHEABLE depends on data. It must not be HIT. - $dynamic_cache = !empty(array_intersect(['user', 'session'], $this->getExpectedCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; - $this->assertResourceResponse(200, NULL, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), FALSE, $dynamic_cache); + $dynamic_cache = !empty(array_intersect(['user', 'session'], $this->getExpectedCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; + $this->assertResourceResponse(200, NULL, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); $head_headers = $response->getHeaders(); // 200 for well-formed GET request. Page Cache hit because of HEAD request. // Same for Dynamic Page Cache hit. $response = $this->request('GET', $url, $request_options); - $this->assertResourceResponse(200, $this->getExpectedDocument(), $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), FALSE, $dynamic_cache === 'MISS' ? 'HIT' : 'UNCACHEABLE'); + $this->assertResourceResponse(200, $this->getExpectedDocument(), $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache === 'MISS' ? 'HIT' : 'UNCACHEABLE (poor cacheability)'); // Assert that Dynamic Page Cache did not store a ResourceResponse object, // which needs serialization after every cache hit. Instead, it should // contain a flattened response. Otherwise performance suffers. @@ -1012,7 +1011,7 @@ public function testGetIndividual(): void { $this->assertInstanceOf(CacheableResponseInterface::class, $cached_response); } } - $this->assertSame($dynamic_cache !== 'UNCACHEABLE' || isset($dynamic_cache_label_only) && $dynamic_cache_label_only !== 'UNCACHEABLE', $found_cached_200_response); + $this->assertSame($dynamic_cache !== 'UNCACHEABLE (poor cacheability)' || isset($dynamic_cache_label_only) && $dynamic_cache_label_only !== 'UNCACHEABLE (poor cacheability)', $found_cached_200_response); $this->assertTrue($other_cached_responses_are_4xx); // Not only assert the normalization, also assert deserialization of the @@ -1061,12 +1060,12 @@ public function testGetIndividual(): void { $message_url = clone $url; $path = str_replace($random_uuid, '{entity}', $message_url->setAbsolute()->setOptions(['base_url' => '', 'query' => []])->toString()); $message = 'The "entity" parameter was not converted for the path "' . $path . '" (route name: "jsonapi.' . static::$resourceTypeName . '.individual")'; - $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], FALSE, 'UNCACHEABLE'); + $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], 'UNCACHEABLE (request policy)', 'UNCACHEABLE (poor cacheability)'); // DX: when Accept request header is missing, still 404, same response. unset($request_options[RequestOptions::HEADERS]['Accept']); $response = $this->request('GET', $url, $request_options); - $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], FALSE, 'UNCACHEABLE'); + $this->assertResourceErrorResponse(404, $message, $url, $response, FALSE, ['4xx-response', 'http_response'], ['url.query_args', 'url.site'], 'UNCACHEABLE (request policy)', 'UNCACHEABLE (poor cacheability)'); } /** @@ -1087,8 +1086,8 @@ public function testCollection(): void { $expected_cacheability = $expected_response->getCacheableMetadata(); $response = $this->request('HEAD', $collection_url, $request_options); // MISS or UNCACHEABLE depends on the collection data. It must not be HIT. - $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; - $this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; + $this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); // Different databases have different sort orders, so a sort is required so // test expectations do not need to vary per database. @@ -1101,10 +1100,10 @@ public function testCollection(): void { $expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options); $expected_cacheability = $expected_response->getCacheableMetadata(); // MISS or UNCACHEABLE depends on the collection data. It must not be HIT. - $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; $expected_document = $expected_response->getResponseData(); $response = $this->request('GET', $collection_url, $request_options); - $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); + $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); $this->setUpAuthorization('GET'); @@ -1112,9 +1111,9 @@ public function testCollection(): void { $expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options); $expected_cacheability = $expected_response->getCacheableMetadata(); // MISS or UNCACHEABLE depends on the collection data. It must not be HIT. - $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; $response = $this->request('HEAD', $collection_url, $request_options); - $this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); + $this->assertResourceResponse(200, NULL, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); // 200 for well-formed GET request. $expected_response = $this->getExpectedCollectionResponse($entity_collection, $collection_url->toString(), $request_options); @@ -1122,8 +1121,8 @@ public function testCollection(): void { $expected_document = $expected_response->getResponseData(); $response = $this->request('GET', $collection_url, $request_options); // Dynamic Page Cache HIT unless the HEAD request was UNCACHEABLE. - $dynamic_cache = $dynamic_cache === 'UNCACHEABLE' ? 'UNCACHEABLE' : 'HIT'; - $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); + $dynamic_cache = $dynamic_cache === 'UNCACHEABLE (poor cacheability)' ? 'UNCACHEABLE (poor cacheability)' : 'HIT'; + $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); if ($this->entity instanceof FieldableEntityInterface) { // 403 for filtering on an unauthorized field on the base resource type. @@ -1145,14 +1144,14 @@ public function testCollection(): void { 'url.site', 'user.permissions', ]; - $this->assertResourceErrorResponse(403, $expected_error_message, $unauthorized_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); + $this->assertResourceErrorResponse(403, $expected_error_message, $unauthorized_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, 'UNCACHEABLE (request policy)', 'MISS'); $this->grantPermissionsToTestedRole(['field_jsonapi_test_entity_ref view access']); // 403 for filtering on an unauthorized field on a related resource type. $response = $this->request('GET', $unauthorized_filter_url, $request_options); $expected_error_message = "The current user is not authorized to filter by the `status` field, given in the path `field_jsonapi_test_entity_ref.entity:user.status`."; - $this->assertResourceErrorResponse(403, $expected_error_message, $unauthorized_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, FALSE, 'MISS'); + $this->assertResourceErrorResponse(403, $expected_error_message, $unauthorized_filter_url, $response, FALSE, $expected_cache_tags, $expected_cache_contexts, 'UNCACHEABLE (request policy)', 'MISS'); } // Remove an entity from the collection, then filter it out. @@ -1176,8 +1175,8 @@ public function testCollection(): void { $expected_document = $expected_response->getResponseData(); $response = $this->request('GET', $filtered_collection_url, $request_options); // MISS or UNCACHEABLE depends on the collection data. It must not be HIT. - $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; - $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; + $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); // Filtered collection with includes. $relationship_field_names = array_reduce($filtered_entity_collection, function ($relationship_field_names, $entity) { @@ -1192,8 +1191,8 @@ public function testCollection(): void { $expected_document = $expected_response->getResponseData(); $response = $this->request('GET', $filtered_collection_include_url, $request_options); // MISS or UNCACHEABLE depends on the included data. It must not be HIT. - $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; - $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; + $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); // If the response should vary by a user's authorizations, grant permissions // for the included resources and execute another request. @@ -1212,8 +1211,8 @@ public function testCollection(): void { $response = $this->request('GET', $filtered_collection_include_url, $request_options); $requires_include_only_permissions = !empty($flattened_permissions); $uncacheable = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())); - $dynamic_cache = !$uncacheable ? $requires_include_only_permissions ? 'MISS' : 'HIT' : 'UNCACHEABLE'; - $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); + $dynamic_cache = !$uncacheable ? $requires_include_only_permissions ? 'MISS' : 'HIT' : 'UNCACHEABLE (poor cacheability)'; + $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); } // Sorted collection with includes. @@ -1230,8 +1229,8 @@ public function testCollection(): void { $expected_document = $expected_response->getResponseData(); $response = $this->request('GET', $sorted_collection_include_url, $request_options); // MISS or UNCACHEABLE depends on the included data. It must not be HIT. - $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 ? 'UNCACHEABLE' : 'MISS'; - $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); + $dynamic_cache = $expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; + $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache); } /** @@ -1387,8 +1386,8 @@ protected function doTestRelated(array $request_options) { $expected_cacheability->getCacheContexts(), FALSE, $actual_response->getStatusCode() === 200 - ? ($expected_cacheability->getCacheMaxAge() === 0 ? 'UNCACHEABLE' : 'MISS') - : (!empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : FALSE) + ? ($expected_cacheability->getCacheMaxAge() === 0 ? 'UNCACHEABLE (poor cacheability)' : 'MISS') + : (!empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : FALSE) ); } } @@ -1422,10 +1421,10 @@ protected function doTestRelationshipGet(array $request_options) { $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), - FALSE, + 'UNCACHEABLE (request policy)', empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? $expected_resource_response->isSuccessful() ? 'MISS' : FALSE - : 'UNCACHEABLE' + : 'UNCACHEABLE (poor cacheability)' ); } } @@ -2725,21 +2724,21 @@ protected function doTestSparseFieldSets(Url $url, array $request_options) { $response = $this->request('GET', $url, $request_options); // Dynamic Page Cache MISS because cache should vary based on the 'field' // query param. (Or uncacheable if expensive cache context.) - $dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; $this->assertResourceResponse( 200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), - FALSE, + 'UNCACHEABLE (request policy)', $dynamic_cache ); } // Test Dynamic Page Cache HIT for a query with the same field set (unless // expensive cache context is present). $response = $this->request('GET', $url, $request_options); - $this->assertResourceResponse(200, FALSE, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache === 'MISS' ? 'HIT' : 'UNCACHEABLE'); + $this->assertResourceResponse(200, FALSE, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'UNCACHEABLE (request policy)', $dynamic_cache === 'MISS' ? 'HIT' : 'UNCACHEABLE (poor cacheability)'); } /** @@ -2784,7 +2783,7 @@ protected function doTestIncluded(Url $url, array $request_options) { // 'include' query param. $expected_cacheability = $expected_response->getCacheableMetadata(); // MISS or UNCACHEABLE depends on data. It must not be HIT. - $dynamic_cache = ($expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $this->getExpectedCacheContexts()))) ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = ($expected_cacheability->getCacheMaxAge() === 0 || !empty(array_intersect(['user', 'session'], $this->getExpectedCacheContexts()))) ? FALSE : 'MISS'; $this->assertResourceResponse( 200, $expected_document, @@ -2896,7 +2895,7 @@ public function testRevisions(): void { $detail .= ' ' . $reason; } // MISS or UNCACHEABLE depends on data. It must not be HIT. - $dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; $this->assertResourceErrorResponse(403, $detail, $url, $actual_response, '/data', $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); // Ensure that targeting a revision does not bypass access. @@ -2907,7 +2906,7 @@ public function testRevisions(): void { $detail .= ' ' . $reason; } // MISS or UNCACHEABLE depends on data. It must not be HIT. - $dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; $this->assertResourceErrorResponse(403, $detail, $url, $actual_response, '/data', $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); $this->setUpRevisionAuthorization('GET'); @@ -3239,7 +3238,7 @@ public function testRevisions(): void { $expected_cacheability = $expected_response->getCacheableMetadata(); $expected_document['links']['self']['href'] = $related_url->toString(); // MISS or UNCACHEABLE depends on data. It must not be HIT. - $dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = !empty(array_intersect(['user', 'session'], $expected_cacheability->getCacheContexts())) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; $expected_cache_tags = !in_array($relationship_type, $default_revision_types, TRUE) ? Cache::mergeTags($expected_cacheability->getCacheTags(), $this->getExtraRevisionCacheTags()) : $expected_cacheability->getCacheTags(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cache_tags, $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache); } diff --git a/core/modules/language/tests/src/Functional/LanguageBrowserDetectionAcceptLanguageTest.php b/core/modules/language/tests/src/Functional/LanguageBrowserDetectionAcceptLanguageTest.php index 157db6ab33b1..d02c9cec4cc2 100644 --- a/core/modules/language/tests/src/Functional/LanguageBrowserDetectionAcceptLanguageTest.php +++ b/core/modules/language/tests/src/Functional/LanguageBrowserDetectionAcceptLanguageTest.php @@ -87,32 +87,32 @@ public function testAcceptLanguageEmptyDefault(): void { $this->drupalGet('/system-test/echo/language test', [], ['Accept-Language' => 'en']); $this->assertSession()->responseHeaderEquals('Content-Language', 'en'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (response policy)'); // Check with UK browser. $this->drupalGet('/system-test/echo/language test', [], ['Accept-Language' => 'en-UK,en']); $this->assertSession()->responseHeaderEquals('Content-Language', 'en'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (response policy)'); // Check with french browser. $this->drupalGet('/system-test/echo/language test', [], ['Accept-Language' => 'fr-FR,fr']); $this->assertSession()->responseHeaderEquals('Content-Language', 'fr'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (response policy)'); // Check with browser without language settings - should return fallback language. $this->drupalGet('/system-test/echo/language test', [], ['Accept-Language' => '']); $this->assertSession()->responseHeaderEquals('Content-Language', 'en'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (response policy)'); // Check with french browser again. $this->drupalGet('/system-test/echo/language test', [], ['Accept-Language' => 'fr-FR,fr']); $this->assertSession()->responseHeaderEquals('Content-Language', 'fr'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (response policy)'); // Check with UK browser. $this->drupalGet('/system-test/echo/language test', [], ['Accept-Language' => 'en-UK,en']); $this->assertSession()->responseHeaderEquals('Content-Language', 'en'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (response policy)'); // Check if prefixed URLs are still cached. $this->drupalGet('/en/system-test/echo/language test', [], ['Accept-Language' => 'en']); diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index 3988454611cc..71b44fe40d55 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -85,7 +85,7 @@ public static function providerTestLayoutSectionFormatter() { ], 'user', 'user:2', - 'UNCACHEABLE', + 'UNCACHEABLE (poor cacheability)', ]; $data['block_with_entity_context'] = [ [ @@ -179,7 +179,7 @@ public function testLayoutSectionFormatter($layout_data, $expected_selector, $ex $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache); $this->drupalGet($canonical_url->toString() . '/layout'); - $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE'); + $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE (poor cacheability)'); } /** @@ -200,14 +200,14 @@ public function testLayoutSectionFormatterAccess(): void { $this->container->get('state')->set('test_block_access', FALSE); $this->drupalGet($node->toUrl('canonical')); - $this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE'); + $this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE (poor cacheability)'); // Ensure the block was not rendered. $this->assertSession()->pageTextNotContains('Hello test world'); // Grant access to the block, and ensure it was rendered. $this->container->get('state')->set('test_block_access', TRUE); $this->drupalGet($node->toUrl('canonical')); - $this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE'); + $this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE (poor cacheability)'); } /** @@ -294,7 +294,8 @@ public function testLayoutDeletingBundle(): void { * @param string $expected_cache_tags * A string of cache tags to be found in the header. * @param string $expected_dynamic_cache - * The expected dynamic cache header. Either 'HIT', 'MISS' or 'UNCACHEABLE'. + * The expected dynamic cache header. Either 'HIT', 'MISS' or + * 'UNCACHEABLE (poor cacheability)'. * * @internal */ diff --git a/core/modules/node/tests/src/Functional/NodeBlockFunctionalTest.php b/core/modules/node/tests/src/Functional/NodeBlockFunctionalTest.php index 0b0287f1ac36..cda97950c642 100644 --- a/core/modules/node/tests/src/Functional/NodeBlockFunctionalTest.php +++ b/core/modules/node/tests/src/Functional/NodeBlockFunctionalTest.php @@ -185,7 +185,7 @@ public function testRecentNodeBlock(): void { $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'session', 'theme', 'url.path', 'url.query_args', 'user', 'route']); // The node/add/article page is an admin path and currently uncacheable. - $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'UNCACHEABLE'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'UNCACHEABLE (poor cacheability)'); $this->drupalGet('node/' . $node1->id()); // Check that block is displayed on the node page when node is of type diff --git a/core/modules/page_cache/src/StackMiddleware/PageCache.php b/core/modules/page_cache/src/StackMiddleware/PageCache.php index efe4e3b99c29..2d0d3d3b0512 100644 --- a/core/modules/page_cache/src/StackMiddleware/PageCache.php +++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php @@ -19,6 +19,11 @@ */ class PageCache implements HttpKernelInterface { + /** + * Name of Page Cache's response header. + */ + const HEADER = 'X-Drupal-Cache'; + /** * The wrapped HTTP kernel. * @@ -83,6 +88,11 @@ public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TR } else { $response = $this->pass($request, $type, $catch); + // Don't indicate non-cacheability on responses to uncacheable requests. + // @see https://tools.ietf.org/html/rfc7231#section-4.2.3 + if ($request->isMethodCacheable()) { + $response->headers->set(static::HEADER, 'UNCACHEABLE (request policy)'); + } } return $response; @@ -122,7 +132,7 @@ protected function pass(Request $request, $type = self::MAIN_REQUEST, $catch = T */ protected function lookup(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE) { if ($response = $this->get($request)) { - $response->headers->set('X-Drupal-Cache', 'HIT'); + $response->headers->set(static::HEADER, 'HIT'); } else { $response = $this->fetch($request, $type, $catch); @@ -193,7 +203,7 @@ protected function fetch(Request $request, $type = self::MAIN_REQUEST, $catch = // Only set the 'X-Drupal-Cache' header if caching is allowed for this // response. if ($this->storeResponse($request, $response)) { - $response->headers->set('X-Drupal-Cache', 'MISS'); + $response->headers->set(static::HEADER, 'MISS'); } return $response; @@ -233,6 +243,7 @@ protected function storeResponse(Request $request, Response $response) { // so by replacing/extending this middleware service or adding another // one. if (!$response instanceof CacheableResponseInterface) { + $response->headers->set(static::HEADER, 'UNCACHEABLE (no cacheability)'); return FALSE; } @@ -246,6 +257,7 @@ protected function storeResponse(Request $request, Response $response) { // Allow policy rules to further restrict which responses to cache. if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { + $response->headers->set(static::HEADER, 'UNCACHEABLE (response policy)'); return FALSE; } diff --git a/core/modules/page_cache/tests/src/Functional/PageCacheTest.php b/core/modules/page_cache/tests/src/Functional/PageCacheTest.php index 6a01a8bfce96..1cbb2cf0c5a3 100644 --- a/core/modules/page_cache/tests/src/Functional/PageCacheTest.php +++ b/core/modules/page_cache/tests/src/Functional/PageCacheTest.php @@ -215,7 +215,7 @@ public function testConditionalRequests(): void { $this->drupalGet('', [], ['If-Modified-Since' => $last_modified, 'If-None-Match' => $etag]); $this->assertSession()->statusCodeEquals(200); // Verify that absence of Page was not cached. - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); $this->drupalLogout(); } @@ -279,7 +279,7 @@ protected function testPageCacheHeaders(): void { $user = $this->drupalCreateUser(); $this->drupalLogin($user); $this->drupalGet('system-test/set-header', ['query' => ['name' => 'Foo', 'value' => 'bar']]); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); $this->assertSession()->responseHeaderNotContains('Vary', 'cookie'); $this->assertSession()->responseHeaderEquals('Cache-Control', 'must-revalidate, no-cache, private'); $this->assertSession()->responseHeaderEquals('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT'); @@ -472,22 +472,22 @@ public function testCacheableResponseResponses(): void { // GET a URL, which would be marked as a cache miss if it were cacheable. $this->drupalGet('/system-test/respond-response'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (no cacheability)'); $this->assertSession()->responseHeaderEquals('Cache-Control', 'must-revalidate, no-cache, private'); // GET it again, verify it's still not cached. $this->drupalGet('/system-test/respond-response'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (no cacheability)'); $this->assertSession()->responseHeaderEquals('Cache-Control', 'must-revalidate, no-cache, private'); // GET a URL, which would be marked as a cache miss if it were cacheable. $this->drupalGet('/system-test/respond-public-response'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (no cacheability)'); $this->assertSession()->responseHeaderEquals('Cache-Control', 'max-age=60, public'); // GET it again, verify it's still not cached. $this->drupalGet('/system-test/respond-public-response'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (no cacheability)'); $this->assertSession()->responseHeaderEquals('Cache-Control', 'max-age=60, public'); // GET a URL, which should be marked as a cache miss. diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 2195c4ea67a9..9e9f7bb89910 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -485,7 +485,7 @@ public function testGet(): void { else { $this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE); } - $this->assertSame(static::$auth ? [] : ['MISS'], $response->getHeader('X-Drupal-Cache')); + $this->assertSame(static::$auth ? ['UNCACHEABLE (request policy)'] : ['MISS'], $response->getHeader('X-Drupal-Cache')); // DX: 403 because unauthorized. $url->setOption('query', ['_format' => static::$format]); $response = $this->request('GET', $url, $request_options); @@ -501,15 +501,15 @@ public function testGet(): void { $response = $this->request('GET', $url, $request_options); if ($has_canonical_url) { $this->assertSame(403, $response->getStatusCode()); - $dynamic_cache = str_starts_with($response->getHeader('X-Drupal-Cache-Max-Age')[0], '0') || !empty(array_intersect(['user', 'session'], explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]))) ? 'UNCACHEABLE' : 'MISS'; + $dynamic_cache = str_starts_with($response->getHeader('X-Drupal-Cache-Max-Age')[0], '0') || !empty(array_intersect(['user', 'session'], explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]))) ? 'UNCACHEABLE (poor cacheability)' : 'MISS'; $this->assertSame([$dynamic_cache], $response->getHeader('X-Drupal-Dynamic-Cache')); } else { $this->assertSame(406, $response->getStatusCode()); - $this->assertSame(['UNCACHEABLE'], $response->getHeader('X-Drupal-Dynamic-Cache')); + $this->assertSame(['UNCACHEABLE (poor cacheability)'], $response->getHeader('X-Drupal-Dynamic-Cache')); } $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); - $this->assertSame(static::$auth ? [] : ['MISS'], $response->getHeader('X-Drupal-Cache')); + $this->assertSame(static::$auth ? ['UNCACHEABLE (request policy)'] : ['MISS'], $response->getHeader('X-Drupal-Cache')); // DX: 403 because unauthorized. $url->setOption('query', ['_format' => static::$format]); $response = $this->request('GET', $url, $request_options); @@ -524,13 +524,13 @@ public function testGet(): void { // 200 for well-formed HEAD request. $response = $this->request('HEAD', $url, $request_options); $is_cacheable_by_dynamic_page_cache = empty(array_intersect(['user', 'session'], $this->getExpectedCacheContexts())); - $this->assertResourceResponse(200, '', $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', $is_cacheable_by_dynamic_page_cache ? 'MISS' : 'UNCACHEABLE'); + $this->assertResourceResponse(200, '', $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', $is_cacheable_by_dynamic_page_cache ? 'MISS' : 'UNCACHEABLE (poor cacheability)'); $head_headers = $response->getHeaders(); // 200 for well-formed GET request. Page Cache hit because of HEAD request. // Same for Dynamic Page Cache hit. $response = $this->request('GET', $url, $request_options); - $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'HIT', $is_cacheable_by_dynamic_page_cache ? (static::$auth ? 'HIT' : 'MISS') : 'UNCACHEABLE'); + $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'HIT', $is_cacheable_by_dynamic_page_cache ? (static::$auth ? 'HIT' : 'MISS') : 'UNCACHEABLE (poor cacheability)'); // Assert that Dynamic Page Cache did not store a ResourceResponse object, // which needs serialization after every cache hit. Instead, it should // contain a flattened response. Otherwise performance suffers. @@ -633,7 +633,7 @@ public function testGet(): void { // DX: upon re-enabling a resource, immediate 200. $response = $this->request('GET', $url, $request_options); - $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', $is_cacheable_by_dynamic_page_cache ? 'MISS' : 'UNCACHEABLE'); + $this->assertResourceResponse(200, FALSE, $response, $this->getExpectedCacheTags(), $this->getExpectedCacheContexts(), static::$auth ? FALSE : 'MISS', $is_cacheable_by_dynamic_page_cache ? 'MISS' : 'UNCACHEABLE (poor cacheability)'); $this->resourceConfigStorage->load(static::$resourceConfigId)->delete(); $this->refreshTestStateAfterRestConfigChange(); @@ -824,7 +824,7 @@ public function testPost(): void { else { $this->assertSame([], $response->getHeader('Location')); } - $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + $this->assertSame([], $response->getHeader('X-Drupal-Cache-Max-Age')); // If the entity is stored, perform extra checks. if (get_class($this->entityStorage) !== ContentEntityNullStorage::class) { // Assert that the entity was indeed created, and that the response body @@ -1073,7 +1073,6 @@ public function testPatch(): void { // 200 for well-formed request. $response = $this->request('PATCH', $url, $request_options); $this->assertResourceResponse(200, FALSE, $response); - $this->assertFalse($response->hasHeader('X-Drupal-Cache')); // Assert that the entity was indeed updated, and that the response body // contains the serialized updated entity. $updated_entity = $this->entityStorage->loadUnchanged($this->entity->id()); diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php index 08a44956f41c..adc598f905ff 100644 --- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php @@ -386,8 +386,8 @@ protected function assertResourceResponse($expected_status_code, $expected_body, $this->assertTrue($response->hasHeader('X-Drupal-Cache')); $this->assertSame($expected_page_cache_header_value, $response->getHeader('X-Drupal-Cache')[0]); } - else { - $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + elseif ($response->hasHeader('X-Drupal-Cache')) { + $this->assertMatchesRegularExpression('#^UNCACHEABLE \((no cacheability|(request|response) policy)\)$#', $response->getHeader('X-Drupal-Cache')[0]); } // Expected Dynamic Page Cache header value: X-Drupal-Dynamic-Cache header. @@ -395,8 +395,8 @@ protected function assertResourceResponse($expected_status_code, $expected_body, $this->assertTrue($response->hasHeader('X-Drupal-Dynamic-Cache')); $this->assertSame($expected_dynamic_page_cache_header_value, $response->getHeader('X-Drupal-Dynamic-Cache')[0]); } - else { - $this->assertFalse($response->hasHeader('X-Drupal-Dynamic-Cache')); + elseif ($response->hasHeader('X-Drupal-Dynamic-Cache')) { + $this->assertMatchesRegularExpression('#^UNCACHEABLE \(((no|poor) cacheability|(request|response) policy)\)$#', $response->getHeader('X-Drupal-Dynamic-Cache')[0]); } } diff --git a/core/modules/system/tests/src/Functional/Session/SessionTest.php b/core/modules/system/tests/src/Functional/Session/SessionTest.php index 03b695adcb82..0ae64de4e1bf 100644 --- a/core/modules/system/tests/src/Functional/Session/SessionTest.php +++ b/core/modules/system/tests/src/Functional/Session/SessionTest.php @@ -216,7 +216,7 @@ public function testEmptyAnonymousSession(): void { $this->assertSessionCookie(FALSE); $this->assertSessionEmpty(FALSE); // Verify that caching was bypassed. - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); $this->assertSession()->pageTextContains('This is a dummy message.'); // Verify that session cookie was deleted. $this->assertSession()->responseHeaderMatches('Set-Cookie', '/SESS\w+=deleted/'); diff --git a/core/modules/system/tests/src/Functional/System/ErrorHandlerTest.php b/core/modules/system/tests/src/Functional/System/ErrorHandlerTest.php index 664fbe1c234b..9bc4cab3cf1d 100644 --- a/core/modules/system/tests/src/Functional/System/ErrorHandlerTest.php +++ b/core/modules/system/tests/src/Functional/System/ErrorHandlerTest.php @@ -166,7 +166,7 @@ public function testExceptionHandler(): void { ->save(); $this->drupalGet('error-test/trigger-exception'); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (no cacheability)'); $this->assertSession()->responseHeaderNotContains('Cache-Control', 'public'); $this->assertSession()->statusCodeEquals(500); $this->assertNoErrorMessage($error_exception); diff --git a/core/modules/user/tests/src/Functional/UserPasswordResetTest.php b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php index b8dd558c8010..7ad09f53748b 100644 --- a/core/modules/user/tests/src/Functional/UserPasswordResetTest.php +++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php @@ -122,11 +122,11 @@ public function testUserPasswordReset(): void { // Ensure that the current URL does not contain the hash and timestamp. $this->assertSession()->addressEquals(Url::fromRoute('user.reset.form', ['uid' => $this->account->id()])); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); // Ensure the password reset URL is not cached. $this->drupalGet($resetURL); - $this->assertSession()->responseHeaderDoesNotExist('X-Drupal-Cache'); + $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)'); // Check the one-time login page. $this->assertSession()->pageTextContains($this->account->getAccountName()); diff --git a/core/profiles/standard/tests/src/Traits/StandardTestTrait.php b/core/profiles/standard/tests/src/Traits/StandardTestTrait.php index be8721bbe5fa..3f67accf8a21 100644 --- a/core/profiles/standard/tests/src/Traits/StandardTestTrait.php +++ b/core/profiles/standard/tests/src/Traits/StandardTestTrait.php @@ -203,7 +203,7 @@ function (ConstraintViolation $v) { $this->drupalGet($url); // Verify that site-wide contact page cannot be cached by Dynamic Page // Cache. - $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE'); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'UNCACHEABLE (poor cacheability)'); $url = Url::fromRoute('<front>'); $this->drupalGet($url); -- GitLab