Problem/Motivation

Follow-up from #2257709: Remove the interdependence between the internal page cache and management of the Cache-Control header for external caches.


Currently the UNIX timestamp is used for the ETag header when serving pages cacheable by proxies and/or the browser. This does not strictly conform to the specification (RFC 2616, Section 13.3) because the ETag is expected to be a strong validator. The UNIX timestamp does not qualify as a strong validator. In order to comply with the RFC, it is either necessary to explicitly mark the ETag as a being weak or alternatively compute a hash of the content and use that.
(removed from the scope of this issue, there is no immediate need to address this here. Alternative approach: #2225765: Consider using cache tags (and cache contexts) for the ETag).

Upon cache-revalidation the method used to check whether a 304 Not modified can be issued does not really conform to the spec according to @Wim Leers (#2257709-5: Remove the interdependence between the internal page cache and management of the Cache-Control header for external caches):

It is not required for both If-Modified-Since and If-None-Match to be specified to trigger a 304 response. If both an ETag and a Last-Modified response header are specified, then UAs SHOULD use both for HTTP revalidation, but it's not a requirement.

In fact the Symfony Response object already supports cache-revalidation out of the box via the isNotModified() method.

However, using this method unmodified would effectively revert #327269: drupal_page_cache_header() should compare timestamp. This needs further investigation (sifting through the Symfony issue tracker, testing with browsers / proxies).

All problems have been addressed upstream.

Proposed resolution

Replace custom revalidation logic with Response::isNotModified().

Remaining tasks

User interface changes

API changes

Comments

znerol’s picture

Regarding HTTP revalidation, this is from the HTTP/1.1 RFC:

   HTTP applications have historically allowed three different formats
   for the representation of date/time stamps:

      Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
      Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
      Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format

   The first format is preferred as an Internet standard and represents
   a fixed-length subset of that defined by RFC 1123 [8] (an update to
   RFC 822 [9]). The second format is in common use, but is based on the
   obsolete RFC 850 [12] date format and lacks a four-digit year.
   HTTP/1.1 clients and servers that parse the date value MUST accept
   all three formats (for compatibility with HTTP/1.0), though they MUST
   only generate the RFC 1123 format for representing HTTP-date values
   in header fields. See section 19.3 for further information.

      Note: Recipients of date values are encouraged to be robust in
      accepting date values that may have been sent by non-HTTP
      applications, as is sometimes the case when retrieving or posting
      messages via proxies/gateways to SMTP or NNTP.

   All HTTP date/time stamps MUST be represented in Greenwich Mean Time
   (GMT), without exception. For the purposes of HTTP, GMT is exactly
   equal to UTC (Coordinated Universal Time).

Let's see how other software implement this requirement:

  • Symfony HeaderBag::getDate() only parses RFC 1123 (in fact they use the constant DATE_RFC2822 but its the same format)
  • Varnish TIM_parse() iterates over all dates specified in the RFC and additionally parses ISO 8601.

Note that the If-Modified-Since header is not even parsed into a date upon HTTP revalidation in Symfony, but the actual string is compared to the Last-Modified header of the response.

The fact that the RFC encourages for robustness in HTTP applications may have lead to #327269: drupal_page_cache_header() should compare timestamp. Against this background (and also compared to Varnish), the Symfony-code looks quite brittle. However, it is very important to keep in mind that conditional requests are HTTP/1.1 only. This means that any client issuing conditional requests is in fact required to send RFC 1123 formatted dates in GMT.

Thus I think string comparison of If-Modified-Since and Last-Modified is sufficient and therefore we safely can substitute the custom HTTP revalidation code with Symfonys Response::isNotModified() (and fix our tests accordingly).

Crell’s picture

If we're concerned, we can also submit a PR upstream to make Symfony more forgiving on inbound formats. That would be the correct way to handle it.

znerol’s picture

There is no reason to be concerned. The Symfony mechanism is perfect, supporting pre-HTTP/1.1 date formats in conditional requests is not necessary.

wim leers’s picture

Wow, #1 is super, super interesting to read. Thanks for that :)

catch’s picture

#2225765: Consider using cache tags (and cache contexts) for the ETag is related if we wanted to move away from timestamp.

I don't think a hash of the actual content works for us, we have $as_time_ago date formatting and other gotchas.

If we stick with a timestamp for the Etag, I think we should consider moving to microtime(), there's no guarantee that a page doesn't get a real update within a second.

znerol’s picture

znerol’s picture

We have $as_time_ago date formatting and other gotchas.

Time ago formatting does not really play well with any long-living markup cache. Except when the interval is formatted on the client using JavaScript (basically what the contrib Timeago module does).

I do not see why a hash wouldn't work for us. The only requirement an ETag needs to fulfill (apart from it being syntactically correct) is that it has to change whenever the underlying resource changes.

damien tournoud’s picture

This the definition of a strong validator:

Since both origin servers and caches will compare two validators to
decide if they represent the same or different entities, one normally
would expect that if the entity (the entity-body or any entity-
headers) changes in any way, then the associated validator would
change as well.
If this is true, then we call this validator a
"strong validator."

(emphasis mine)

Our time-based validator is a strong validator (at least with one-second granularity).

znerol’s picture

Scroll down a little bit and there is:

Note: One example of a strong validator is an integer that is
incremented in stable storage every time an entity is changed.
An entity's modification time, if represented with one-second
resolution, could be a weak validator
, since it is possible that
the resource might be modified twice during a single second.

catch’s picture

We can easily fix that particular issue by using microtime() per #5.

damien tournoud’s picture

My point is, you don't need a strong validator to be idempotent. you only need it to change when the content changes.

znerol’s picture

you only need it to change when the content changes.

This is exactly what somehash($content); does with the additional benefit that we do not depend on (micro)time. Having a stable algorithm to generate the ETag also helps with testing.

What exactly is it that makes you think having time in the ETag is better than supplying a hash?

Crell’s picture

Remember that the response in question may not be an HTML page; it could just as easily be a HAL response, a fragment, an image file, or anything else. Not all of those have a modified-time we can leverage. IMO a content hash is the most reliable way to ensure a unique ETag for any arbitrary response.

The point about "time ago" is valid, but is the same issue as, say, a view in a sidebar of recent comments. That can update at any time as well, and presumably if that has changed we DO want to send a new response. Remember we're doing this post-request-handling, so the work of generating the page is already done either way. This is just to save network bytes, for which we should be validating against... the bytes that would be put on the network.

catch’s picture

It should be possible to do this by swapping the page cache out via a middleware - could then check the cache tags and add the invalidations to the header.

Still not sure whether we want to do that by default - long TTLs in Drupal, expiring tags in varnish, with Varnish setting a much shorter expires header upstream seems preferable or at least both are valid approaches.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.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.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.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.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.

armrus’s picture

Version: 8.6.x-dev » 8.8.x-dev
StatusFileSize
new1.71 KB

See the https://tools.ietf.org/html/rfc7232#section-3.3

A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field; the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since, and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match.

But https://tools.ietf.org/html/rfc2616##section-13.3.4 says

An HTTP/1.1 origin server, upon receiving a conditional request that includes both a Last-Modified date (e.g., in an If-Modified-Since or If-Unmodified-Since header field) and one or more entity tags (e.g., in an If-Match, If-None-Match, or If-Range header field) as cache validators, MUST NOT return a response status of 304 (Not Modified) unless doing so is consistent with all of the conditional header fields in the request.

Correct revalidation implemented in Symfony\Component\HttpFoundation\Response::isNotModified().
I try to use it,

There is Patch.

armrus’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work
armrus’s picture

StatusFileSize
new3.6 KB

Fix tests for new logic of cache checks.

armrus’s picture

StatusFileSize
new3.63 KB

Fix tests.

armrus’s picture

Status: Needs work » Needs review
StatusFileSize
new3.73 KB

try to fix login filed test.

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

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

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

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now 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.

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

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

it-cru’s picture

Priority: Normal » Major
Status: Needs review » Needs work

Patch from #26 does not apply against Drupal 8.9.11

Checked 9.2.x files and I think only tests of patch needs adjustment to apply again.

I would like to raise this issue from normal to major.

abhisekmazumdar’s picture

Status: Needs work » Needs review
StatusFileSize
new3.33 KB

Updated the patch for 9.2.x

Status: Needs review » Needs work

The last submitted patch, 31: 2259489-31.patch, failed testing. View results

anmolgoyal74’s picture

Status: Needs work » Needs review
StatusFileSize
new3.25 KB
new1.29 KB

Status: Needs review » Needs work

The last submitted patch, 33: 2259489-33.patch, failed testing. View results

volkerk’s picture

Status: Needs work » Needs review
StatusFileSize
new3.41 KB
new1.44 KB

There are two additional issues:

The RFC 7232 states in Section 3.2 that

A recipient MUST use the weak comparison function when comparing entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags can be used for cache validation even if there have been changes to the representation data.

and in Section 3.3

A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field; the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since, and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match.

This should be fixed upstream, I will try to get https://github.com/symfony/symfony/issues/37948 re-opened and provide a PR there.

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.

volkerk’s picture

PR https://github.com/symfony/symfony/pull/40545 was merged, symfony behavior is now correct.

tobiasb’s picture

So the patch also needs symfony/http-foundation v4.4.30?

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.

chr.fritsch’s picture

Yes, and drupal 9.3.x ships with "v4.4.34"

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.

znerol’s picture

Title: Use strong validator for ETag and leverage symfony for HTTP revalidation » Leverage symfony for HTTP revalidation
Issue summary: View changes
Status: Needs review » Needs work

Thanks!

  1. +++ b/core/modules/page_cache/tests/src/Functional/PageCacheTest.php
    @@ -227,21 +227,19 @@ public function testConditionalRequests() {
    -    // Verify the page is not printed twice when the cache is warm.
    -    $this->assertSession()->responseNotMatches('#<html.*<html#');
    

    The test design wasn't ideal in the beginning. Regrettably deleting those two lines will remove some test coverage. I propose to move those lines to near the top of the test method. So that the start reads something like this:

        [...]
        // Fill the cache.
        $this->drupalGet('');
        // Verify the page is not printed twice when the cache is cold.
        $this->assertSession()->responseNotMatches('#<html.*<html#');
    
        $this->drupalGet('');
        // Verify the page is not printed twice when the cache is warm.
        $this->assertSession()->responseNotMatches('#<html.*<html#');
        $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
        [...]
    

Otherwise this is RTBC.

znerol’s picture

znerol’s picture

Issue tags: +GlobalContributionWeekend2023

Tagging this for global contribution weekend 2023. I hope that somebody likes to pick this up. I'll stick around for questions / reviews.

gambry’s picture

Issue tags: -GlobalContributionWeekend2023 +ContributionWeekend2023

Swapping the Drupal Global Contribution Weekend 2023 tag with the correct one.

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.

hmdnawaz’s picture

StatusFileSize
new3.41 KB

Patch for Drupal 10.3

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.