Problem/Motivation

PageCache determines which responses to cache by inspecting the Expires header. (This should be updated to use the Cache-Control header instead, but that's out of scope here.)

Responses that have Expires: Sun, 19 Nov 1978 05:00:00 GMT are meant to not be cached, because it's an expiration date in the past. Yet PageCache caches them permanently:

$expire = ($date > $request_time) ? $date : Cache::PERMANENT;

This has been the case for a very long time, even predating #2527126: Only send X-Drupal-Cache-Tags and -Contexts headers when developer explicitly enables them. Quite possibly the rationale was it's fine to cache uncacheable responses, because cache tags will ensure the responses are invalidated in a timely manner.


Now, to make matters worse, even when you configure a non-zero "max age" at /admin/config/development/performance, \Drupal\Core\EventSubscriber\FinishResponseSubscriber::setResponseCacheable() will still cause that same Expires header to be set? Why? Because when \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond() calls \Drupal\Core\EventSubscriber\FinishResponseSubscriber::setResponseCacheable(), the latter contains this code:

    // HTTP/1.0 proxies do not support the Vary header, so prevent any caching
    // by sending an Expires date in the past. HTTP/1.1 clients ignore the
    // Expires header if a Cache-Control: max-age directive is specified (see
    // RFC 2616, section 14.9.3).
    if (!$response->headers->has('Expires')) {
      $this->setExpiresNoCache($response);
    }

In other words:

  1. Drupal 8 intentionally disables HTTP/1.0 proxies
  2. Drupal 8 ships with a HTTP/1.0 reverse proxy that is breaking the spec: Page Cache

Proposed resolution

Make \Drupal\page_cache\StackMiddleware\PageCache a HTTP/1.1 proxy: make it inspect Cache-Control rather than Expires

Remaining tasks

TBD

Remaining tasks

TBD

User interface changes

None.

API changes

None.

Data model changes

None.

Issue fork drupal-2835068

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Leo Pitt’s picture

This seems like quite a major issue - but no activity on it in the past two years ...?

Is it still a current issue?

Anonymous’s picture

Version: 8.6.x-dev » 8.7.x-dev

I think it is still current, yes. I continue to find that whenever I want a page to be entirely uncacheable for some reason, I have to both set the max-age to 0 and call the page cache kill switch, even though just setting the max-age should be enough; and when I want a page cache to expire after a particular amount of time, I've been needing to sort of do so with cron and custom cache tags since simply setting max-age doesn't do the trick. My understanding is that this ticket is to resolve this issue and get cache max-age to apply normally to the page cache, the same as cache tags and cache contexts already do.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.9 was released on November 6 and is the final full bugfix release for the Drupal 8.7.x series. Drupal 8.7.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.8.0 on December 4, 2019. (Drupal 8.8.0-beta1 is available for testing.)

Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Nigel Cunningham’s picture

I'm testing the attached patch in combination with the cache control override module and a couple of patches to it (https://www.drupal.org/files/issues/2019-09-26/cache_control_override_ex... and https://www.drupal.org/files/issues/2020-01-09/cache_control_override-31...), to good effect.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

mxr576’s picture

Version: 8.9.x-dev » 9.2.x-dev
oknate’s picture

FileSize
903 bytes

Here's a patch that turns off caching in the page cache if it's set to not cacheable in FinishResponseSubscriber::onRespond().

It's similar to patch #9, but is more explicit.

+      if ($expires == \DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 UTC')) {
+        // If explicitly set to this date, do not cache.
+        return FALSE;
+      }

The only place that date is used in Drupal core is FinishResponseSubscriber.

UPDATE: the next patch I added adds a more clear note about this.

This will make it more obvious to developers that their caching isn't working properly, for example if they've disabled it!

I tried Wim's suggestion to check the max age, but it was set to a negative number in this case. I like the idea of checking the same date to which the FinishResponseSubscriber sets the expires.

I ran into this with the Sitewide Alert module. https://www.drupal.org/project/sitewide_alert/issues/3189144

If you set expires header on a CacheableJsonResponse returned in a controller, like this:

$response->setExpires($expireDate->getPhpDateTime());

but you have set cache.page.max_age to 0 (<no caching>) at /admin/config/development/performance, the CacheableJsonResponse gets mangled in \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond() and set to not cacheable, but Page Cache caches it anyway with 'expire' set to -1. This breaks the scheduling feature. The solution in my case easy, to set the cache.page.max_age to something other than 0.

I was also able to fix it with the kill switch. But I'm glad I figured out that had forgot to set my max-page in the performance settings.

oknate’s picture

Just adding a little more info in the comment.

Wim, can you add a little more info on this suggestion:

Make \Drupal\page_cache\StackMiddleware\PageCache a HTTP/1.1 proxy: make it inspect Cache-Control rather than Expires

Symfony has a \Symfony\Component\HttpFoundation\Response::isCacheable() method that looks to me to be perfect here. I don't see it used in Drupal core at all.

Perhaps instead of checking the expires we could check if it's cacheable using that method.

oknate’s picture

Wim, I don't think the second part of the description is still true:

even when you configure a non-zero "max age" at /admin/config/development/performance, \Drupal\Core\EventSubscriber\FinishResponseSubscriber::setResponseCacheable() will still cause that same Expires header to be set

When I was testing tonight, with a CacheableJsonResponse from a controller, this wasn't the case. I think as long as it the response is an instance of CacheableResponseInterface, and cache.page.max_age is greater than zero, the expires is not altered.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Anatolij Zajika’s picture

Hello everybody. Here is the module that solves this problem and has settings - https://www.drupal.org/project/custom_cache

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

SocialNicheGuru’s picture

Status: Active » Needs review
smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative, +Needs tests

#13 had many failures and was not ready for review.
Which show this will need it's own test case and work for BC.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

codebymikey made their first commit to this issue’s fork.