Problem/Motivation

  1. Many hosting environments (and proxies and reverse proxies and CDNs) impose limits on the header size, and for a sufficiently complex Drupal 8 page, the number of cache tags on that page (i.e. the number of things that page depends on) can cause the cache tags header to go beyond 4 KB, causing WSODs.
  2. For sites without a reverse proxy or CDN, Drupal sending the X-Drupal-* headers by default takes up bandwidth so slows down communication between client and server, even when the headers don't exceed the fixed limits.
  3. For sites using a commercial CDN service, Drupal sending the X-Drupal-* headers by default is not a benefit, because those services expect different, non-Drupal, headers (Fastly: Surrogate-Keys, CloudFlare: Cache-Tag).
  4. For sites using a configurable/scriptable reverse proxy (e.g., Varnish) that could be configured to use X-Drupal-* headers, you would still need a Drupal integration module for sending the invalidation command, so there is little benefit to Drupal core sending the headers vs. letting the integration module do it.

Proposed resolution

  1. Change core to only add the X-Drupal-Cache-Tags and X-Drupal-Cache-Contexts headers when the http.response.debug_cacheability_headers container parameter is set.
  2. Set this parameter in WebTestBase, to preserve the way in which test assertions for cacheability information work.
  3. Change PageCache to only cache CacheableResponseInterface responses rather than extracting cache tags from the X-Drupal-Cache-Tags header.
  4. Remaining tasks

    None.

    User interface changes

    None.

    API changes

    Item #3 from the proposed resolution means that contrib modules that were returning non-CacheableResponseInterface responses (see #126) and expecting them to be cached in Drupal's page cache anyway will no longer get that benefit. An "Http headers" section was added to https://www.drupal.org/node/2562903 to clarify that such expectations are not part of the public API.

    Data model changes

    None.

CommentFileSizeAuthor
#160 interdiff.txt4.01 KBeffulgentsia
#160 2527126-160.patch18.4 KBeffulgentsia
#152 2527126-152.patch16.57 KBdawehner
#152 interdiff.txt940 bytesdawehner
#152 2527126-151.patch15.79 KBdawehner
#143 interdiff.txt7.13 KBWim Leers
#143 interdiff-test-only.txt4.84 KBWim Leers
#143 2527126-143.patch16.01 KBWim Leers
#143 2527126-143-test-only.patch14.63 KBWim Leers
#137 2527126-115.patch10.3 KBWim Leers
#130 interdiff.txt4.62 KBWim Leers
#130 interdiff-test-only.txt3.33 KBWim Leers
#130 2527126-129.patch13.29 KBWim Leers
#130 2527126-129-test-only.patch12.91 KBWim Leers
#115 interdiff.txt3.94 KBWim Leers
#115 2527126-115.patch10.3 KBWim Leers
#112 interdiff.txt1 KBWim Leers
#112 2527126-112.patch6.84 KBWim Leers
#111 interdiff.txt39.59 KBWim Leers
#111 2527126-111.patch7.52 KBWim Leers
#105 interdiff.txt741 bytesWim Leers
#105 2527126-105.patch42.08 KBWim Leers
#103 interdiff.txt4.36 KBWim Leers
#103 2527126-103.patch42.08 KBWim Leers
#101 interdiff.txt41.31 KBWim Leers
#101 2527126-100.patch42 KBWim Leers
#99 interdiff.txt4.32 KBWim Leers
#99 2527126-99.patch40.58 KBWim Leers
#97 interdiff.txt32.47 KBWim Leers
#97 2527126-97.patch38.57 KBWim Leers
#92 interdiff.txt14.27 KBWim Leers
#92 2527126-92.patch45.71 KBWim Leers
#88 interdiff.txt22.19 KBWim Leers
#88 2527126-88.patch27.5 KBWim Leers
#87 interdiff.txt13.42 KBWim Leers
#87 2527126-87.patch7.45 KBWim Leers
#81 interdiff.txt556 bytesdawehner
#81 2527126-81.patch7.68 KBdawehner
#79 interdiff.txt5.53 KBdawehner
#79 2527126-79.patch7.68 KBdawehner
#66 interdiff.txt2.87 KBdawehner
#66 2527126-66.patch6.36 KBdawehner
#63 interdiff.txt2.88 KBdawehner
#63 2527126-63.patch6.4 KBdawehner
#59 interdiff.txt583 bytesdawehner
#59 2527126-59.patch3.52 KBdawehner
#55 interdiff.txt22.92 KBdawehner
#55 2527126-55.patch3.52 KBdawehner
#51 interdiff.txt15.8 KBdawehner
#51 2527126-51.patch23.97 KBdawehner
#46 interdiff.txt571 bytesdawehner
#46 2527126-45.patch10.55 KBdawehner
#40 2527126-40.patch10.53 KBdawehner
#40 interdiff.txt2.21 KBdawehner
#39 2527126-39.patch8.85 KBanavarre
#37 2527126-37.patch10.77 KBjanusman
#35 2527126-35.patch8.93 KBdawehner
#16 interdiff.txt3.01 KBWim Leers
#16 2527126-16.patch2.97 KBWim Leers
#10 interdiff.txt2.44 KBdawehner
#10 2527126-10.patch2.4 KBdawehner
#6 2527126-6.patch1.03 KBdawehner
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dawehner’s picture

While I not necessarily agree with the DDOS argument, there are so many ways anyway by default, I totally agree with the performance point.

#2241377: [meta] Profile/rationalise cache tags actually has currently that problem

hass’s picture

Issue summary: View changes
hass’s picture

Issue summary: View changes
hass’s picture

Issue summary: View changes
hass’s picture

Issue summary: View changes
dawehner’s picture

Status: Active » Needs review
FileSize
1.03 KB

This for sure will fail a LOT, but it this basically what we want?

Wim Leers’s picture

I fear that people only set that setting in case they want the logging to set the right IP. So I think it needs further scrutiny.

But yes, it sure seems that simple :)

Wim Leers’s picture

Also, does this mean you propose having WebTestBase set the reverse_proxy setting to TRUE? Or do you want to introduce a separate flag (config perhaps?) to expose it anyway?

Wim Leers’s picture

Well, we want these headers when debugging anyway, so we want a flag either in Settings or in Config to turn it on. Then settings.local.php can turn it on again.

Large headers slow down communication between client and server.

Indeed! Especially concerning is that this header is in the first 14 KB of the TCP packet, so that means less space for more valuable information.

With the headers attackers may be able to identify sections of a page that are not cached and may start a targeted DDoS.

No. By that argument, literally everything in Drupal 7 is DDoSable, with the sole exception of image styles.

dawehner’s picture

Talked with wim about the name of that debugging flag.

The last submitted patch, 6: 2527126-6.patch, failed testing.

olli’s picture

Manually tested the patch and it seems to work!

+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -117,7 +117,9 @@ public function onRespond(FilterResponseEvent $event) {
+    // We just want to send the cacheability headers in case it is actually
+    // needed, so a reverse proxy is used, or when we want to debug its values.
+    if ((Settings::get('reverse_proxy', FALSE) || Settings::get('send_cacheability_headers', FALSE)) && $response instanceof CacheableResponseInterface) {

Why not only a simple flag to enable the headers without checking reverse_proxy setting?

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

#12: As soon as you use a reverse proxy in 99% of the cases you want the cache tags and cache contexts.

RTBC, we have enough test coverage for that.

jibran’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record, +Needs tests

This needs change record and also tests for this change.

Wim Leers’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record, -Needs tests
FileSize
2.97 KB
3.01 KB

Some rewording for consistency/clarity and nitpicking.

I also agree with Fabianx that we don't need tests, because WebTestBase sets this new setting, and we have dozens upon dozens of tests doing cacheability header assertions, so this'd fail immediately otherwise.

As for a CR, I've updated https://www.drupal.org/node/2259531.

Wim Leers’s picture

Rewrote the IS.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs review

Actually, shouldn't we do this using a container parameter instead of a setting?

dawehner’s picture

In case we want that we should move all of the reverse proxy settings to the container.

Wim Leers’s picture

Perhaps I'm misremembering, but wasn't general rule we don't add NEW things to settings.php? Don't we favor container parameters over that now?

dawehner’s picture

<WimLeers> dawehner: just earlier today, Alex mentioned we're getting rid of settings eventually, right?
<WimLeers> dawehner: Which made me vaguely remember that as a general rule we don't add NEW settings?
<dawehner> WimLeers: the thing is the following
<amateescu> WimLeers: I think it's better to keep together for now, if reverse proxy settings are moved to container param, the new one will be moved as well
<dawehner> WimLeers: once we have the container of fabian we can have dynamic container params
<dawehner> WimLeers: so override container params from settings.php
<WimLeers> ohhhhhhhhhhhh
<WimLeers> that sounds awesome
<dawehner> yeah work for a follow up
<WimLeers> dawehner: can you put that on the issue? I'll re-RTBC.
Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a major bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 7115108 and pushed to 8.0.x. Thanks!

  • alexpott committed 7115108 on 8.0.x
    Issue #2527126 by dawehner, Wim Leers, hass: Only send cache context/...
hass’s picture

Status: Fixed » Needs work
Issue tags: +Needs documentation

Do we have recommended configurations for frontend caches somewhere? I think we need to document this so varnish admins do not miss to remove the headers in their cache servers, too.

dawehner’s picture

Status: Needs work » Fixed
Issue tags: -Needs documentation

Do we have recommended configurations for frontend caches somewhere? I think we need to document this so varnish admins do not miss to remove the headers in their cache servers, too.

This does not belong into this issue. Please create a new one.

Wim Leers’s picture

Wim Leers’s picture

Status: Fixed » Active

This broke the Page Cache. Page Cache cache items now have no cache tags, and thus are never invalidated.

Page Cache now only works if you are either behind a reverse proxy or have that new flag enabled. The reason tests pass is because we made web test enable the new "send cacheability headers" setting.

Fabianx’s picture

Title: Only send cache context/tags if frontend proxy exists » [Needs revert] Only send cache context/tags if frontend proxy exists

Ughhhh, lets revert for now and make page cache use the cacheable metadata on the response instead.

dawehner’s picture

In other words it is a horrible idea to have a different test environment than actual code running in production.
I think we should enable the cache tags debug output in places where we need it.

Wim Leers’s picture

In other words it is a horrible idea to have a different test environment than actual code running in production.

+1000

catch’s picture

Title: [Needs revert] Only send cache context/tags if frontend proxy exists » Only send cache context/tags if frontend proxy exists

Reverted.

  • catch committed 144b803 on 8.0.x
    Revert "Issue #2527126 by dawehner, Wim Leers, hass: Only send cache...
dawehner’s picture

Status: Active » Needs review
FileSize
8.93 KB

For now this removes the autoenabling out of WebTestBase, but this should also get some dedicated test.

Status: Needs review » Needs work

The last submitted patch, 35: 2527126-35.patch, failed testing.

janusman’s picture

Status: Needs work » Needs review
FileSize
10.77 KB

Patch didn't apply. New patch for testing.

Status: Needs review » Needs work

The last submitted patch, 37: 2527126-37.patch, failed testing.

anavarre’s picture

Status: Needs work » Needs review
FileSize
8.85 KB

Attempting a straight reroll from #35.

dawehner’s picture

@anavarre
Ha, good try to simply don't add the new code.

While talking with @Fabianx we realized that we can also strip after the page cache.

Status: Needs review » Needs work

The last submitted patch, 40: 2527126-40.patch, failed testing.

The last submitted patch, 40: 2527126-40.patch, failed testing.

Wim Leers’s picture

While talking with @Fabianx we realized that we can also strip after the page cache.

Indeed. That was the plan all along.

But it needs to be configurable. Very often, it's a good idea to still use Page Cache (and Dynamic Page Cache) even when you're using a CDN, because it still reduces the load on the origin: many edges may simultaneously request the same page from the origin, and then (Dynamic) Page Cache still helps.

Fabianx’s picture

#43: This has nothing to do with dynamic page cache vs. non-dynamic page cache.

It just means we remove the headers in the reverse proxy middleware instead of in the FinishResponseSubscriber.

Hence PageCache and all other things running in a Middleware or ResponseEvent subscriber are fine.

If you use a CDN or varnish, set reverse_proxy = TRUE or set the send_cacheability_headers = TRUE - easy enough.

I think this only needs the part of ensuring our tests have the cacheability headers enabled from #16 as there is nothing using headers after ReverseProxyMiddleware.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
10.56 KB

I only read #40, so I thought it was the Page Cache's middleware that was going to strip it.

Fixing a small oversight that caused those many failures.

dawehner’s picture

FileSize
10.55 KB
571 bytes

Wow, this are actually quite a few failures for not returning a response at all.

The last submitted patch, 45: 2527126-45.patch, failed testing.

The last submitted patch, , failed testing.

Status: Needs review » Needs work

The last submitted patch, 46: 2527126-45.patch, failed testing.

The last submitted patch, 46: 2527126-45.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
23.97 KB
15.8 KB

Status: Needs review » Needs work

The last submitted patch, 51: 2527126-51.patch, failed testing.

The last submitted patch, 51: 2527126-51.patch, failed testing.

Fabianx’s picture

Why not again unconditionally on WebTestBase?

Yes, we screwed up once, but only because we removed those headers too early.

Overall that seems like a way less invasive change and also not breaking contrib tests, which we can't do at this point in time anymore.

dawehner’s picture

Status: Needs work » Needs review
FileSize
3.52 KB
22.92 KB

Well, ulike the last time we should ensure that it still works without them

Status: Needs review » Needs work

The last submitted patch, 55: 2527126-55.patch, failed testing.

The last submitted patch, 55: 2527126-55.patch, failed testing.

Fabianx’s picture

+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -3002,4 +3004,20 @@ protected function assertNoCacheTag($cache_tag) {
+    $settings['settings']['send_cacheability_headers'] = (object) [
+      'value' => TRUE,

Should be 'value' => $value ;).

dawehner’s picture

Status: Needs work » Needs review
FileSize
3.52 KB
583 bytes

Should be 'value' => $value ;).

Ah perfect, thank you for catching that!
First I had two methods but then considered that as a little bit too silly.

Fabianx’s picture

Status: Needs review » Needs work

Fail: [Other] Line 0 of sites/default/files/simpletest/phpunit-1224.xml:
PHPunit Test failed to complete

Unit tests for ReverseProxyMiddlewareTest need to be updated, too - to account for the new calls to settings.

The last submitted patch, 59: 2527126-59.patch, failed testing.

The last submitted patch, 59: 2527126-59.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
6.4 KB
2.88 KB

Sure

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC - finally.

Hope this can go in during RC.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php
    @@ -55,7 +55,17 @@ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch =
    +    // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively, when
    +    // either a reverse proxy is being used (so the reverse proxy or CDN can be
    +    // invalidated when appropriate) or when developing/debugging.
    

    Seems like a verb is missing at the beginning of this.

  2. +++ b/core/modules/simpletest/src/WebTestBase.php
    @@ -3019,4 +3021,20 @@ protected function assertNoCacheTag($cache_tag) {
    +   * Sets the setting whether to send cacheability headers.
    +   *
    +   * @param bool $value
    +   *   (optional) Should the cache headers be send.
    

    Needs language love.

  3. +++ b/core/modules/simpletest/src/WebTestBase.php
    @@ -3019,4 +3021,20 @@ protected function assertNoCacheTag($cache_tag) {
    +  protected function setCacheHeaders($value = TRUE) {
    

    s/setCacheHeaders()/setCacheabilityHeaders()/

  4. +++ b/core/modules/simpletest/src/WebTestBase.php
    @@ -3019,4 +3021,20 @@ protected function assertNoCacheTag($cache_tag) {
    +    // Send cacheability headers so tests can check their values.
    

    Pointless comment.

dawehner’s picture

Status: Needs work » Needs review
FileSize
6.36 KB
2.87 KB

Seems like a verb is missing at the beginning of this.

Yes, there is certainly a word .

Pointless comment.

Agreed.

Status: Needs review » Needs work

The last submitted patch, 66: 2527126-66.patch, failed testing.

webchick’s picture

Shaddup, PIFT.

webchick’s picture

Status: Needs work » Needs review

Ahem.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

Two nits that can be fixed on commit:

  1. +++ b/core/modules/simpletest/src/WebTestBase.php
    @@ -3022,13 +3022,12 @@ protected function assertNoCacheTag($cache_tag) {
        *   (optional) Should the cache headers be send.
    

    Whether the cacheability headers should be sent.

  2. +++ b/sites/example.settings.local.php
    @@ -73,6 +73,13 @@
    + * Send cacheablity headers for debugging purposes.
    

    s/cacheblity/cacheability/


To avoid the "paper bag" release that we had several betas ago, when #16 got committed and turned out to break the Page Cache (see #29). The root cause for this going unnoticed back then was: The reason tests pass is because we made web test enable the new "send cacheability headers" setting.

@dawehner described it very well why that was stupid in #31:

In other words it is a horrible idea to have a different test environment than actual code running in production.
I think we should enable the cache tags debug output in places where we need it.

This time, however, we do it in the reverse proxy middleware that runs after the page cache (it wraps the Page Cache middleware). So we cannot possibly break the Page Cache anymore.

To make that absolutely certain, however, we now test that even when you explicitly set send_cacheability_headers = FALSE, it still continues to work:

+++ b/core/modules/page_cache/src/Tests/PageCacheTest.php
@@ -50,6 +50,10 @@ protected function setUp() {
   function testPageCacheTags() {
+    // Ensure that the sending out of the cache tags / contexts is independent
+    // from the page cache.
+    $this->setCacheabilityHeaders(FALSE);

And finally, I did manual testing, verifying that both the Page Cache and the Dynamic Page Cache, and it all indeed still works out of the box, unlike last time.

effulgentsia’s picture

Issue tags: +rc target

We discussed this on a committer call today and decided that it's better for this to go in during RC than just before, so tagging. While it's unfortunate that some RC testers will thus get WSOD on some hosting environments (according to the issue summary), we expect this to be a very small minority.

dawehner’s picture

---
Well, I think you might underestimate which hosting environments this are.

catch’s picture

If they are big hosting environments that care about Drupal, then they (or we) will get a lot of bug reports until they raise the header limit.

Also if they offer reverse proxies, wouldn't you want to set $settings['reverse_proxy'] = TRUE to take advantage of that? So this ends up being a workaround where you disable all reverse proxy behaviour despite there actually being one on your hosting.

I think we should keep going with issues like #2542868: Allow a header value size limit to be specified regardless of this one.

Wim Leers’s picture

Yep, agreed with the sentiment of #73. This is just a band-aid for the typical use case. It helps improve front-end performance in the process.

But we still need #2542868: Allow a header value size limit to be specified together with this, because if your setup/architecture needs the cacheability headers, then you still need to ensure it doesn't exceed the supported header length.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work

During the D8 EU criticals call of this morning, we (Alex Pott, dawehner, Wim) discussed this issue, and its sister issue #2542868: Allow a header value size limit to be specified.

  1. There are two purposes to the cacheability headers: A) debugging/testing ("is my cacheability metadata showing up?" and ("what cacheability metadata does this response have?"), and B) reverse proxies (efficient invalidation of cached responses).
  2. For purpose A, this does not need to be on by default at all.
  3. For purpose B, just having that header is insufficient, you also need code that talks to the reverse proxy (Varnish, a CDN …) to inform it about the invalidations as they happen. (And given that I've ported both the Fastly and CloudFlare modules to Drupal 8, and added support for this, I can add an additional data point: they each require a specifically named header anyway, so we already need to remove/rename the X-Drupal-Cache-Tags header.)
  4. Therefore, it is pointless that HEAD always sends the cacheability headers.
  5. Therefore, we can have this be an opt-in thing for developers.
  6. … and since you never need to change this on the fly, this can be a container parameter — just like the Twig debug output setting (twig.config.debug), which is a container parameter. i.e. add a new container parameter to core.services.yml, document it in default.services.yml.

NW for that simpler, clearer, more sensible direction.

Wim Leers’s picture

Note that with #75's approach, the necessity of #2542868: Allow a header value size limit to be specified becomes questionable.

dawehner’s picture

one thing we could do:

  1. add a new module http_headers, which takes a cacheable response and adds the HTTP headers to it, basically what #2542868: Allow a header value size limit to be specified partially does
  2. let page_cache require it
  3. Make it enabled in most tests

Sadly this is not the minimal change.

Wim Leers’s picture

One clarification: PageCache doesn't need the X-Drupal-Cache-Tags header anymore. It currently parses that header to get the cache tags of the response, but, ever since we got CacheableResponse, that's not necessary anymore. That's why it's totally fine to not even have the X-Drupal-Cache-(Tags|Contexts) headers even when (Dynamic) Page Cache is enabled: because the response already carries the cacheability metadata in a more structured form.

dawehner’s picture

Status: Needs work » Needs review
FileSize
7.68 KB
5.53 KB

Yeah I agree this would not be the minimal change at all.

Status: Needs review » Needs work

The last submitted patch, 79: 2527126-79.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
7.68 KB
556 bytes

Small details matter.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers
Status: Needs review » Needs work
  1. +++ b/core/core.services.yml
    @@ -16,6 +16,7 @@ parameters:
    +  http.cacheability_headers.send: false
    

    I like the name :) http makes sense.

    cacheability_headers makes sense.

    But perhaps send should become debug? That'd match twig.config's debug parameter better. But that also doesn't sound quite as good.

    Hrm, not sure… Ideas?

  2. --- a/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php
    +++ b/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php
    --- a/core/tests/Drupal/Tests/Core/StackMiddleware/ReverseProxyMiddlewareTest.php
    +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/ReverseProxyMiddlewareTest.php
    

    Well, these changes don't really make sense anymore after #75's conclusion, IMO.

    We can make it simpler still: don't generate these headers at all… unless the container parameter tells us to.

    Then there is no reason for the reverse proxy middleware to strip them.

  3. +++ b/sites/default/default.services.yml
    @@ -142,3 +142,7 @@ parameters:
    +  # Send cacheablity headers for debugging purposes.
    +  # By default, cacheability headers are only sent when behind a reverse proxy.
    +  # http.cacheability_headers.send: false
    

    Nit: Rather than appending this to the end of the file, I think it makes more sense to put after Twig & Renderer's settings.

Working on addressing all that.

Fabianx’s picture

#78 is unfortunately wrong.

We specifically also support other responses right now - that send cache tags and max-age values themselves.

Overall I am okay to remove unless that container parameter or setting is set though - and making that opt-in.

Wim Leers’s picture

#83: that's not intentional, that's pure coincidence. There's no test coverage verifying that. It's not supported.

dawehner’s picture

Well, just to be clear, I think people should be able to add the tags, if needed but we should simply not add them in the beginning.

znerol’s picture

+++ b/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php
@@ -55,7 +65,18 @@ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch =
+    // Strip X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header
+    // respectively, when either a reverse proxy is being used (so the reverse
+    // proxy or CDN can be invalidated when appropriate) or when
+    // developing/debugging.

The comment does not seem to match the implemented logic. How about:

Leave the X-Drupal-CacheContexts and X-Drupal-Cache-Tags header on the response when either a reverse proxy is being used or when developing/debugging. Otherwise remove them.

Wim Leers’s picture

Implemented #82.

This will cause any test relying on X-Drupal-Cache-(Tags|Contexts) to fail. I'll update the other tests in the next reroll. That keeps this patch easier to review, and it's this patch that contains the crucial changes.

Wim Leers’s picture

FileSize
27.5 KB
22.19 KB

And now updated every test that uses assert(No)Cache(Tag|Context)().

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
  1. +++ b/core/core.services.yml
    @@ -16,6 +16,7 @@ parameters:
    +  http.cacheability_headers.send: false
    

    I'm still not fully convinced by this name.

    I wonder if it'd be better to stress the "debug" aspect of this more?

    Then perhaps http.cacheable_response.debug_headers: false or something like that would make more sense?

  2. +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php
    @@ -257,6 +257,9 @@ function testPageCache() {
    +    $this->setCacheabilityHeaders(TRUE);
    

    I think "set cacheability headers" is also relatively confusing, I think "send cacheability headers" would be better? Of course, the answer to this point depends on the feedback to the first point.

The last submitted patch, 87: 2527126-87.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 88: 2527126-88.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
45.71 KB
14.27 KB

Status: Needs review » Needs work

The last submitted patch, 92: 2527126-92.patch, failed testing.

dawehner’s picture

+++ b/core/modules/menu_ui/src/Tests/MenuTest.php
@@ -543,6 +543,9 @@ function testSystemMenuRename() {
+    $this->setCacheabilityHeaders(TRUE);
+    $this->rebuildContainer();

Should we move the rebuilderContainer() code into the set... call? I think this would make the code a little bit less repetitive.

I think "set cacheability headers" is also relatively confusing, I think "send cacheability headers" would be better? Of course, the answer to this point depends on the feedback to the first point.

Well, I don't care that much. Afaik it should mirror the parameter as much as possible and be done, so set(Camelize($parameter_name))

I'm still not fully convinced by this name.

I wonder if it'd be better to stress the "debug" aspect of this more?

Then perhaps http.cacheable_response.debug_headers: false or something like that would make more sense?

Its a total odd thing that the cacheability information are part of the FinalResponseSubscriber, which makes it a little bit confusing to find a proper name here. I would argue that the cachability information should be moved to the list bit, as its about displaying cachability debug headers, not any kind of headers, so what about http.response.debug_cacheability_headers?

catch’s picture

The setting being called cacheability headers suggests it'd include Cache-control, so something more specific sounds good.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

Should we move the rebuilderContainer() code into the set... call? I think this would make the code a little bit less repetitive.

+1

http.response.debug_cacheability_headers

Hah, that's exactly what I had written locally in a patch already, before I decided to first gather feedback :) +1

The setting being called cacheability headers suggests it'd include Cache-control, so something more specific sounds good.

Exactly! :)


Assigning to myself to address those points.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
38.57 KB
32.47 KB

Should we move the rebuilderContainer() code into the set... call? I think this would make the code a little bit less repetitive.

Done.

Also rebased, #92 didn't have a single fail like DrupalCI misleadingly suggested: the patch simply did not apply anymore.

The rest will follow tomorrow.

Status: Needs review » Needs work

The last submitted patch, 97: 2527126-97.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
40.58 KB
4.32 KB

This will hopefully be the first green patch.

The nastiest remaining problem was:

Exception: _theme() may not be called until all modules are loaded. in Drupal\Core\Theme\ThemeManager->render() (line 145 of /var/www/html/core/lib/Drupal/Core/Theme/ThemeManager.php). Drupal\Core\Theme\ThemeManager->render('field', Array)

the rebuildContainer() call caused $this->moduleHandler->isLoaded() to return FALSE. A call to resetAll() fixes that (that's also how WebTestBase::setUp() works).

tstoeckler’s picture

+++ b/sites/default/default.services.yml
@@ -115,6 +115,9 @@ parameters:
+  # By default, cacheability headers are only sent when behind a reverse proxy.

This seems to be no longer true?

Wim Leers’s picture

Component: cache system » request processing system
Assigned: Wim Leers » Unassigned
FileSize
42 KB
41.31 KB

To be able to link to proper documentation, I wrote https://www.drupal.org/developing/api/8/response and particularly created the https://www.drupal.org/developing/api/8/response/cacheable-response-inte... child page. I also updated https://www.drupal.org/developing/api/8/cache and https://www.drupal.org/developing/api/8/render/arrays/cacheability#headers accordingly. And I added https://www.drupal.org/developing/api/8/cache/cacheable-response-interface.


With all that out the way, I was able to finally able to rename the container parameter to http.response.debug_cacheability_headers and rename the method on WebTestBase accordingly.

Status: Needs review » Needs work

The last submitted patch, 101: 2527126-100.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
42.08 KB
4.36 KB

Well that was rather stupid: I renamed it everywhere, except where it's actually being used.

tstoeckler’s picture

+++ b/sites/default/default.services.yml
@@ -115,9 +115,17 @@ parameters:
+  # For more information about debugging cacheable responses, se

se -> see

Wim Leers’s picture

FileSize
42.08 KB
741 bytes

#104: Thanks, fixed now! #100 was fixed in #101.

moshe weitzman’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Fixed up the IS, after chatting with Wim. Looks solid to me.

Wim Leers’s picture

Title: Only send cache context/tags if frontend proxy exists » Only send cache context/tags when developer explicitly enables them
Issue summary: View changes

Forgot to adjust the issue title for the new direction since #75.

Minor update to IS.

alexpott’s picture

Is there any reason why we're not turning the headers on in all test environments where they matter - this would limit the impact on any contrib or custom tests that depend on this.

dawehner’s picture

Is there any reason why we're not turning the headers on in all test environments where they matter - this would limit the impact on any contrib or custom tests that depend on this.

Well, I was originally in favour of doing that, but it required quite a lot of enabling, see https://www.drupal.org/files/issues/2527126-51.patch as a start (wasn't everything),
so it would not be the minimal change, this is for sure.

alexpott’s picture

@dawehner the latest patch is doing this everywhere:

+++ b/core/modules/block/src/Tests/Views/DisplayBlockTest.php
@@ -241,6 +241,8 @@ public function testViewsBlockForm() {
   public function testBlockRendering() {
+    $this->setHttpResponseDebugCacheabilityHeaders(TRUE);

Why don't we just enable this for KernelTestBases, WebTestBase and BrowserTestBase - for all tests.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
7.52 KB
39.59 KB

#110: Because then the tests don't match reality. And that's what caused Page Cache to be broken in beta 15 in the first place. See #29 and #31:

In other words it is a horrible idea to have a different test environment than actual code running in production.
I think we should enable the cache tags debug output in places where we need it.

Of course…

  1. with the approach in the current patch, it's actually impossible to break the Page Cache by enabling/disabling these headers, because it doesn't rely on them anymore.
  2. and the current patch breaks every cacheability test, in both core and contrib.

So… I think I'm +1 to @alexpott's proposal. This is probably the most reasonable approach.

Wim Leers’s picture

FileSize
6.84 KB
1 KB
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -3019,4 +3023,18 @@ protected function assertNoCacheTag($cache_tag) {
+  /**
+   * Enables/disables the cacheability headers.
+   *
+   * Sets the http.response.debug_cacheability_headers container parameter.
+   *
+   * @param bool $value
+   *   (optional) Whether the debugging cacheability headers should be sent.
+   */
+  protected function setHttpResponseDebugCacheabilityHeaders($value = TRUE) {
+    $this->setContainerParameter('http.response.debug_cacheability_headers', $value);
+    $this->rebuildContainer();
+    $this->resetAll();
+  }

In fact, this is now something that is necessary for only the most exotic tests. Nothing in core uses it.

So let's remove that too.

dawehner’s picture

Status: Needs review » Needs work
So let's remove that too.

/me sighs loud.

We need a test for the parameter ... to ensure that a) we don't send out the header, unless needed (we had that in earlier versions of the patch, I'm quite sure about that)
b) page caching is still working if the parameter is set to FALSE (we had a critical regression, let's ensure this doesn't happen anymore).

Wim Leers’s picture

Issue summary: View changes

IMO this is ready again now. With a smaller patch than ever before. And now with zero disruption.

IS updated again. https://www.drupal.org/node/2259531 updated.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
10.3 KB
3.94 KB

Okay, adding an explicit regression test for Page Cache. I don't think that's particularly helpful now, but totally agreed: better safe than sorry in this regard :)

Fully agreed on adding a test that verifies the debugging cacheability headers are sent or not when the container parameter is set to true or false respectively.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Great, thank you!

Fabianx’s picture

RTBC - I will miss looking at D8 websites and see their contexts online ;) - but yes using CachableResponseInterface is much simpler overall.

znerol’s picture

+1, a lot cleaner than before.

borisson_’s picture

I like this solution, it's very clear.

I wonder if we should remove the X-Drupal-Dynamic-Cache header as well? While that one isn't as big as the contexts/tags header I think this serves the same purpose: debugging / development.
We can do that in a followup though.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record

I think we might need an explicit change record for https://www.drupal.org/node/2527126 in case anyone has a reverse proxy that is using the tags OR is implementing a module

Wim Leers’s picture

Title: Only send cache context/tags when developer explicitly enables them » Only send X-Drupal-Cache-Tags and -Contexts headers when developer explicitly enables them
Issue summary: View changes
Status: Needs work » Reviewed & tested by the community
Issue tags: -Security improvements, -Needs change record

#117:

RTBC - I will miss looking at D8 websites and see their contexts online ;) - but yes using CachableResponseInterface is much simpler overall.

+1 :)


#119:

I wonder if we should remove the X-Drupal-Dynamic-Cache header as well?

I see your point, but I think we don't want that, because:

  • There's no danger of WSODs with that header.
  • We also always expose the X-Drupal-Cache header for Page Cache, so it's consistent with that.
  • There's no additional logic/module necessary for that header to be useful.

#120: CR created: https://www.drupal.org/node/2592471


Again improved the IS, the title, and the issue tags.

dawehner’s picture

@Wim Leers
Why is that no longer a security improvement? I mean of course its kind of small one but I think its not the worst to not shout out internal IDs to the public everywhere.

Wim Leers’s picture

This never was about security. The OP added that tag, and it should've been removed a long time ago. When we added this header originally, we had it thoroughly vetted by the security team. The conclusion was there was no security risk at all. If there were, we would never have sent this header by default.

dawehner’s picture

Ah well, the original issue summary had it, but it got deleted ...

Wim Leers’s picture

#124: it's still there, but crossed through, because it's wrong/silly.

It's super easy to find uncached things to "DDoS" sites: just append a random query string to any Drupal URL that returns a HTML response. You can fill up the Page Cache with millions of duplicate frontpage entries with just a different query string. This is well-known, and applies to all Drupal versions.

That's much more damaging than knowing which entities would cause a certain response to change, because an attacker would not be able to modify those entities. If they can modify those entities, they don't need these cache tags to know that anyway.

Therefore, that argument is IMHO entirely moot.

Berdir’s picture

From request.module's RedirectRequestSubscriber:

      $headers = [
        'X-Redirect-ID' => $redirect->id(),
        'X-Drupal-Cache-Tags' => implode(' ', $redirect->getCacheTags()),
      ];
      $response = new TrustedRedirectResponse($url->setAbsolute()->toString(), $redirect->getStatusCode(), $headers);

This change *will* break that.

I'm a bit confused now, do we now have a cacheable redirect response object or not? I think that wasn't committed yet?

I could implement my own I guess but maybe we can find a way to keep BC there (we are in RC, after all..) by falling back to the header if it doesn't implement the interface?

Wim Leers’s picture

I'm a bit confused now, do we now have a cacheable redirect response object or not? I think that wasn't committed yet?

No, it was not: #2573923: Introduce a CacheableRedirectResponse and use it where appropriate.

But redirect.module could have its own ConfiguredRedirectResponse extends SecuredRedirectResponse implements CacheableResponseInterface {} implementation so that it fully controls the response.

I could implement my own I guess but maybe we can find a way to keep BC there (we are in RC, after all..) by falling back to the header if it doesn't implement the interface?

I think you maintain the only module that does this.

You're right that we're breaking the ability to create any kind of response that has this header and have it work with Page Cache. But doing that allows us to have a very simple explanation: Page Cache and Dynamic Page Cache support cacheability metadata if the response contains cacheability metadata, i.e. if it implements CacheableResponseInterface. That's very easy to understand.

Do you think there are (many) other modules that would be affected by this?

Berdir’s picture

I think you maintain the only module that does this.

Why do I hear that all the time ;)

I probably am, as far as contrib is concerned.. who knows what custom code is doing out there already.

You're right that we're breaking the ability to create any kind of response that has this header and have it work with Page Cache. But doing that allows us to have a very simple explanation: Page Cache and Dynamic Page Cache support cacheability metadata if the response contains cacheability metadata, i.e. if it implements CacheableResponseInterface. That's very easy to understand.

Yes, I fully agree that is much easier. But... RC ;) I think we need to be very, very careful in regards to breaking things unless there is no other way. We've had plenty of freezes, beta's and so on, and every single time, within days/weeks we more or less fell back into the old pattern of breaking things all over the place. That really has to stop now.

And this wouldn't be a complicated to do in a way that keeps BC. Just have an if/else with a @todo to remove it in 9.x.

catch’s picture

Status: Reviewed & tested by the community » Needs review

So it is true that it breaks the module.

I'm not sure the implementation detail of 1. the naming of the header 2. internal page cache consuming that header is something we should consider an API though.

That is more for #2550249: [meta] Document @internal APIs both explicitly in phpdoc and implicitly in d.o documentation - clearly we did not document the contents/consumption of custom headers there.

Wim Leers’s picture

Fair.

Done.

IS updated accordingly.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC - the BC layer makes sense to me.

The last submitted patch, 130: 2527126-129-test-only.patch, failed testing.

The last submitted patch, 130: 2527126-129-test-only.patch, failed testing.

effulgentsia’s picture

+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -812,6 +812,10 @@ protected function initSettings() {
+    // During tests, cacheable responses should get the debugging cacheability
+    // headers by default.
+    $this->setContainerParameter('http.response.debug_cacheability_headers', TRUE);
...
+++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
@@ -224,7 +225,17 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch
+    // The above (superior) mechanism to retrieve a response's cache tags was
+    // introduced during the RC phase. This exists to maintain backwards
+    // compatibility.
+    // @todo Remove in Drupal 9.0.0.
+    elseif ($response->headers->has('X-Drupal-Cache-Tags')) {
+      $tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
+    }

I don't like this combination. Both the name of the parameter and the first hunk setting it differently for tests than the production default implies that the 'X-Drupal-Cache-Tags' header is for debugging. But the second hunk means that there's a non-debugging aspect to the header as well. That then puts us into some fuzzy territory with potential for bugs and WTFs.

I see a couple ways out of this:

  1. Either accept breaking a little BC, and remove that second hunk. But in order to not make use cases like #126 result in invalid caching, make PageCache only cache CacheableResponseInterface responses.
  2. Or accept that 'X-Drupal-Cache-Tags' is not a debugging header, but a functional header, and revert PageCache to what it is in HEAD. And instead of solving this overall issue with a container parameter that implies "debugging", solve it with another middleware that runs at a higher priority than PageCache and strips out the headers.

Not changing issue status in case another committer disagrees with me and wants to commit as-is.

Wim Leers’s picture

But the second hunk means that there's a non-debugging aspect to the header as well. That then puts us into some fuzzy territory with potential for bugs and WTFs.

I completely agree.

2. […] And instead of solving this overall issue with a container parameter that implies "debugging", solve it with another middleware that runs at a higher priority than PageCache and strips out the headers.

More middlewares mean worse performance.

I vote for option 1, of course.

catch’s picture

Status: Reviewed & tested by the community » Needs work

Discussed in irc with berdir.

I personally don't think the custom header counts as an API - if we'd thought of it prior to rc, it'd be documented in https://www.drupal.org/node/2562903 (which I've just added a note to for any similar issues that come up in the future).

However, before this issue lands (or as part of it) we should ensure that for path redirect, redirect responses stop getting cached altogether.

i.e. if we'd never had that header, then path_redirect would not have been able to use it, so the only option would have been to not have any redirect caching. That fact that we cache redirects in core now without #2573923: Introduce a CacheableRedirectResponse and use it where appropriate is a bug. That might have been a contributed project blocker, but it wouldn't have been any kind of API change then. Sometimes nothing is better than something...

Approached that way, it's a bug fix to stop caching any redirects, and an API addition to start caching some of them. And path_redirect then has some dead code that relied on a core implementation detail (and via berdir also broken tests that were testing the behaviour). Just breaking tests doesn't necessarily mean we changed an API though, a string change can break a test too.

For that reason I think we should not add a bc layer here, because it's an 'API that should never have been', but we need to think very carefully about this and make sure the documentation reflects any decisions taken like this. And it'd be good to make sure people on this issue actually agree with those arguments - this is going to come up more places than here and we need to be as consistent as we can.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
10.3 KB

#136++

Per #136, re-uploading #115. Interdiff compared to #129 is then of course the inverse of #129's interdiff.

alexpott’s picture

Status: Needs review » Reviewed & tested by the community

It looks like we've considered every possible angle. We've tried to maintain a BC layer but as per @effulgentsia this ends up being more confusing and breaking redirect caching is sensible given that this was relying on a side effect rather than an API. Given the discussions here I think we should agree that #2573923: Introduce a CacheableRedirectResponse and use it where appropriate is an RC target.

alexpott’s picture

Status: Reviewed & tested by the community » Postponed
Related issues: +#2573923: Introduce a CacheableRedirectResponse and use it where appropriate

@catch had the idea of postponing this on #2573923: Introduce a CacheableRedirectResponse and use it where appropriate. I agree. That way we have no regression.

Fabianx’s picture

Status: Postponed » Needs review

#126 is actually not cached in page cache, because of #2476407: Use CacheableResponseInterface to determine which responses should be cached, which went in before RC and did affect both page cache and caching in reverse proxies.

So unless contrib also modified the Expires and Cache-Control headers - which is not the case for redirect module, then the point of BC is very moot.

If however this continues to be cached - despite #2476407: Use CacheableResponseInterface to determine which responses should be cached then that is a bug in PageCache - however that is unlikely.

Fabianx’s picture

Status: Needs review » Postponed

Opened #2593887: Follow-up: Do not cache private or no-store responses in PageCache, berdirs tests passing is a bug. They should be failing by now.

effulgentsia’s picture

+++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
@@ -224,7 +225,7 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch
-    $tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
+    $tags = ($response instanceof CacheableResponseInterface) ? $response->getCacheableMetadata()->getCacheTags() : [];
     $this->set($request, $response, $expire, $tags);

This doesn't quite implement my suggestion in #134.1. My suggestion there is that the ->set() should not be done for a $response that isn't CacheableResponseInterface.

Following on #140, I don't think #2476407: Use CacheableResponseInterface to determine which responses should be cached is enough of an equivalent to that. Because that issue still allows a Response that isn't a CacheableResponseInterface to use Cache-Control headers and therefore be cacheable by something, which is correct. But say such a custom Response also sets some other headers that manage tag-like invalidation (e.g., Surrogate-Keys, if this is a custom module that's catered to a particular deployment architecture). In such a case, since PageCache doesn't know about such headers, it should simply not be in the business of trying to cache such a Response.

Or in other words, modules that want to work with caching generically rather than deployment-specifically should return CacheableResponseInterface responses. Then PageCache can cache it and various contrib modules can provide additional middlewares to transform CacheableResponseInterface information to the headers needed by a corresponding proxy/CDN. But, if a module chooses to not return CacheableResponseInterface, but instead manages its own response headers, then PageCache should skip over it and leave caching to whatever software that module catered the headers to.

Wim Leers’s picture

Status: Postponed » Needs review
FileSize
14.63 KB
16.01 KB
4.84 KB
7.13 KB

#142 is eminently sensible. It'd make the entire response caching infrastructure in Drupal 8 super simple to understand:

Drupal 8 itself will only ever cache CacheableResponseInterface responses. In both the Page Cache and Dynamic Page Cache.

(i.e. Dynamic Page Cache already has this restriction.)

This patch implements that, and adds the necessary test coverage. (And fixes the broken bits in \Drupal\page_cache\Tests\PageCacheTest::testCacheableResponseResponses().)


#140: I found why #2476407: Use CacheableResponseInterface to determine which responses should be cached's test coverage did not do what you expected it to do: because the assertions do

$this->assertFalse(in_array('X-Drupal-Cache', $this->drupalGetHeaders()), 'Drupal page cache header not found');

… but $this->drupalGetHeaders() returns lower-cased header names.

The last submitted patch, 143: 2527126-143-test-only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 143: 2527126-143.patch, failed testing.

Berdir’s picture

  1. +++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
    @@ -220,12 +230,9 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch
    +    $max_age = $response->getCacheableMetadata()->getCacheMaxAge();
    +    $expire = ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $request->server->get('REQUEST_TIME') + $max_age;
    

    Not sure about this change, I think that should be done/discussed separately?

    So that we can test what this actually means for a standard install or when you add statistics.module to the mix (which now defines a max age of 3600 by default if you can view the node counter. Should have test coverage and so on...

  2. +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php
    @@ -420,15 +451,25 @@ public function testCacheableResponseResponses() {
         // Try to fill the cache.
         $this->drupalGet('/system-test/respond-reponse');
    -    $this->assertFalse(in_array('X-Drupal-Cache', $this->drupalGetHeaders()), 'Drupal page cache header not found');
    ...
         // Try to fill the cache.
    +    $this->drupalGet('/system-test/respond-public-response');
    
    @@ -444,12 +485,8 @@ public function testCacheableResponseResponses() {
         // Try to fill the cache.
    

    I think that comment is still weird, considering that we don't actually try to do that?

Other than that, looks great.

tstoeckler’s picture

+++ b/sites/default/default.services.yml
@@ -115,6 +115,17 @@ parameters:
+  http.response.debug_cacheability_headers: false

Would be cool to add this to development.services.yml with a true value IMO. (That might also mitigate some of @FabianX's sadness ;-))

Can be a follow-up, though, and should in case that's in any way controversial.

Wim Leers’s picture

#146.1: can you explain what the problem with that is?

Wim Leers’s picture

Status: Needs work » Needs review

I can't reproduce the fail in https://www.drupal.org/files/issues/2527126-143.patch locally… re-testing.

The last submitted patch, 143: 2527126-143-test-only.patch, failed testing.

Wim Leers’s picture

#146

  1. I don't see what's controversial about that TBH? #2352009: [pp-3] Bubbling of elements' max-age to the page's headers and the page cache is what is controversial, because it also affects the header that we send out to end users, and thus will affect how long stale content may be shown to the user. This on the other hand is still invalidated instantaneously thanks to cache tags and merely ensures that the responses cached are refreshed more often.

    Can you explain in what way this could be problematic? Happy to add test coverage. Happy to defer to a follow-up too, but I want to understand it better.

  2. Agreed that those comments were very confusing. Fixed all of them.

#147: I'm afraid that is highly controversial. More so because if this would be in there, then twig.config.debug etc should be too. I totally see your reasoning though: it currently requires too many steps/is too confusing to enable any of those "debug" container parameters. Could you file a separate issue for that?

dawehner’s picture

Would be cool to add this to development.services.yml with a true value IMO. (That might also mitigate some of @FabianX's sadness ;-))

Great idea!

Not sure about this change, I think that should be done/discussed separately?

You are totally right IMHO. This bit does NOT solve the issue we have, its a separate issue which needs separate tests. Due to the usage of time() vs. REQUEST_TIME the testbot might exceed the time a bit. When I debugged it locally it most of the time one test failure occured, but on some circumstances the debugger slowed it down even more, so that sometimes even two test failed. Let's revert that bit, its not needed in this particular issue.

Berdir’s picture

(crosspost with #150/151)

1. I'm not sure there is a problem, but it is IMHO an unrelated change, one that needs a dedicated issue and testing. It changes behavior that is not related to this issue but #2352009: [pp-3] Bubbling of elements' max-age to the page's headers and the page cache.

To see one example of how this changes things: install statistics.module, enable content view counter, give permission to anon users. Without this patch, page cache doesn't care and caches forever. With this patch, the expire is now set to 1h. That's a great improvement, actually, but it has nothing to with this issue IMHO. Lets do that in the issue mentioned above where we should also discuss how Cache-Control: max-age should behave exactly, right now, that doesn't respect this either.

dawehner’s picture

#147: I'm afraid that is highly controversial. More so because if this would be in there, then twig.config.debug etc should be too. I totally see your reasoning though: it currently requires too many steps/is too confusing to enable any of those "debug" container parameters. Could you file a separate issue for that?

Well, here is one: #2594543: Put http.response.debug_cacheability_headers into default.services.yml ...

Fabianx’s picture

+1 to this direction and thanks for fixing the test. Given this direction I think I can close #2593887: Follow-up: Do not cache private or no-store responses in PageCache as a duplicate of this :).

Or maybe we should do the cache only cacheable responses in its own issue first?

Wim Leers’s picture

#152 + #153: ahh, of course! Thanks for the explanation, both of you! :)

#154: thanks for filing that!

#155: No, that is still necessary. Or is at least a valuable thing to discuss, come to consensus, and formalize the expectations in the form of a test. Because even if a response implements CacheableResponseInterface, if it's marked as private or no-store, it could be argued that Page Cache should in fact not cache it.


#152 looks great to me.

znerol’s picture

+++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
@@ -208,10 +209,19 @@ protected function lookup(Request $request, $type = self::MASTER_REQUEST, $catch
+    // Page Cache only works with cacheable responses. It does not work with
+    // plain Response objects.
+    if (!$response instanceof CacheableResponseInterface) {
+      return $response;
+    }

If this is really the case, then please give some justification at least in the issue summary, but probably also in the comment.

Update: I understand that this comes from #142, but still needs documentation.

Wim Leers’s picture

#2573923: Introduce a CacheableRedirectResponse and use it where appropriate landed a few hours ago, so that blocker is out of the way.

@effulgentsia is going to reply to #157.

Then we can hopefully go to RTBC.

dawehner’s picture

As long the final response object can be used to extract somehow those caching information I'm happy with it, because well, I had usecases to fetch these information already to be honest.

effulgentsia’s picture

This adds the docs requested in #157. I'm open to feedback on how to improve it or make it more concise.

effulgentsia’s picture

+++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
@@ -207,33 +207,56 @@
+    // - Custom applications that wish to provide internal page cache support
+    //   for responses that do not implement CacheableResponseInterface may do
+    //   so by replacing/extending this middleware service or adding another
+    //   one.
     if (!$response instanceof CacheableResponseInterface) {
       return $response;
     }

When I wrote this, I thought, hm, maybe this check should be moved to a response policy instead of being baked into this class. But I'm unclear on whether the page_cache_response_policy service tag is to only control what is cached by page_cache module, or whether those policies should also control what gets cached by reverse proxies and CDNs (via the corresponding integration module). If the latter, then this should not move to a response policy, since we only want this affecting page_cache itself. Do we have/need an issue to discuss/document this aspect of response policies?

effulgentsia’s picture

Issue summary: View changes

Updated summary to reflect the #160 patch. The last update was in #130, so was behind by several patches.

Wim Leers’s picture

When I wrote this, I thought, hm, maybe this check should be moved to a response policy instead of being baked into this class.

Doing so would not be an API change. It would be a behavior change that results in the same exact behavior by default, but would open the door to sites removing or changing this policy. Which means this would be a pure API addition. So we could do so at a later time, when the need arises.

It's better to start simpler & more strict and add more abilities later.

Do we have/need an issue to discuss/document this aspect of response policies?

I think we need to discuss that, yes. Given that these are used by both the PageCache middleware and the FinishResponseSubscriber to determine the Cache-Control headers that are sent to the world, the page_cache_response_policy policies actually do affect both Drupal's internal page cache and external proxies.

I think it's worth taking this aspect into consideration in #2540684: Rename Drupal\Core\PageCache namespace, so I propose we continue that discussion there.

Fabianx’s picture

#163: Also the FinishResponseSubscriber already has a check hard-coded for CacheableDependencyInterface and marks the response as uncacheable then (unless headers have been modified).

And unless we want to support the full HTTP spec like Symfony's HttpCache middleware, I don't think it would be wise to store anything besides CacheableResponseInterface responses, where we can say:

Just like smart_cache, just like render_cache. _Only_ CacheableDependencyInterface information is taken into account.

That makes it (as Wim has already said several times) very simple and easy to understand.

So +1 to hard-coding for now.

Fabianx’s picture

RTBC + 1 for #160 - not sure what is missing here, so not setting status value, but patch looks good to me.

dawehner’s picture

+++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
@@ -206,25 +207,55 @@ protected function lookup(Request $request, $type = self::MASTER_REQUEST, $catch
+    // encode cache tags in a response. Different CDNs implement their own
+    // approaches, and configurable reverse proxies (e.g., Varnish) allow for
+    // custom implementations. To keep Drupal's internal page cache simple, we
+    // only cache CacheableResponseInterface responses, since those provide a
+    // defined API for retrieving cache tags. For responses that do not
+    // implement CacheableResponseInterface, there's no easy way to distinguish
+    // responses that truly don't depend on any site data from responses that
+    // contain invalidation information customized to a particular proxy or
+    // CDN.
+    // - Drupal modules are encouraged to use CacheableResponseInterface
+    //   responses where possible and to leave the encoding of that information
+    //   into response headers to the corresponding proxy/CDN integration
+    //   modules.
+    // - Custom applications that wish to provide internal page cache support
+    //   for responses that do not implement CacheableResponseInterface may do
+    //   so by replacing/extending this middleware service or adding another
+    //   one.
+    if (!$response instanceof CacheableResponseInterface) {
+      return $response;
+    }
+
+    // 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) {

So can anyone explain me actually why anyone of this is in scope of this issue? The issue is that we sent out a too big HEADER, and this is all. Other changes should live in its own issue, IMHO

znerol’s picture

Status: Needs review » Reviewed & tested by the community

#157 is addressed, I think #153 and previous concerns from @Berdir are addressed as well according to #2573923-76: Introduce a CacheableRedirectResponse and use it where appropriate and the change record looks right.

Wim Leers’s picture

  • Given that this was already RTBC at #116 by dawehner and confirmed in #117, #118, #119 by three others (Fabianx, znerol, borisson_)
  • Alex Pott then RTBC'd in #138 but un-RTBC'd one comment later to postpone on #2573923: Introduce a CacheableRedirectResponse and use it where appropriate, which has landed since
  • effulgentsia raised a concern in #142 that we should be even stricter/simpler/clearer about what to cache; that's what the reroll in #143 was for; the subsequent few rerolls refined that and minimized the amount of change

That means every single concern here has been addressed and carefully weighed against all other concerns. We have minimal changes, extremely carefully determined decisions.

What's holding back RTBC?


EDIT: cross-posted with #166. Reply to that: Because we are no longer sending the X-Drupal-Cache-Tags header always, which means PageCache cannot rely on it, which means it must rely on CacheableResponseInterface responses' metadata, which led to further discussions that are inherently intertwined with that change, which then after carefully achieved consensus has resulted in this. Ideally, yes, it would live in a separate issue, but it's so closely intertwined that it doesn't really make sense to split it up IMO.


EDIT: cross-posted with #167 too. Thanks @znerol.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Thanks everyone - good discussion of what to do here with respect to backwards compatibility - and sorry @Berdir for breaking yet another of your modules but at least this time we had the solution in first.

Committed ff622b5 and pushed to 8.0.x. Thanks!

  • alexpott committed ff622b5 on 8.0.x
    Issue #2527126 by Wim Leers, dawehner, effulgentsia, anavarre, janusman...

The last submitted patch, 143: 2527126-143-test-only.patch, failed testing.

The last submitted patch, 143: 2527126-143.patch, failed testing.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

The last submitted patch, 16: 2527126-16.patch, failed testing.

geerlingguy’s picture

I have some follow-up comments to the changes in this issue / change record, posted over in #2241377-77: [meta] Profile/rationalise cache tags; basically, if I'm using Varnish (or some other reverse proxy) and would like to have a header to use for cache tags, is it recommended I somehow build my own, or should I enable http.response.debug_cacheability_headers in production (which the documentation now in services.yml recommends against) and use the built-in X-Drupal-Cache-Tags header?

It seems like I would want to use Drupal's built-in header, but since the setting has the word debug in it and a scary warning about "Not recommended in production environments"... I'm kind of at a loss.

Wim Leers’s picture

Yes, send your own. The CR already explains that very explicitly. See https://www.drupal.org/node/2592471.

I'll state the reason *again* then: because different reverse proxies have different expectations wrt the name of the header and/or the format of the value of the header.

Wim Leers’s picture

jeetmail72’s picture

How to disable X-Drupal-Cache-Tags from Header in Drupal 8

Steps:
1. Check which services.yml file is being loading from your settings.php or settings.local.php
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/services.yml';
2. Open services.yml file and search
http.response.debug_cacheability_headers: false
It should be false for the Prod environment.

Now check your website headers.