Problem/Motivation
Follow-up from #2257709: Remove the interdependence between the internal page cache and management of the Cache-Control header for external caches.
(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).
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.
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-SinceandIf-None-Matchto be specified to trigger a 304 response. If both anETagand aLast-Modifiedresponse 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
| Comment | File | Size | Author |
|---|---|---|---|
| #48 | 2259489-48.patch | 3.41 KB | hmdnawaz |
| #35 | interdiff_33-35.txt | 1.44 KB | volkerk |
| #35 | 2259489-35.patch | 3.41 KB | volkerk |
| #26 | 2259489-try-symfonys-is-not-modified-method-26.patch | 3.73 KB | armrus |
Comments
Comment #1
znerol commentedRegarding HTTP revalidation, this is from the HTTP/1.1 RFC:
Let's see how other software implement this requirement:
DATE_RFC2822but its the same format)Note that the
If-Modified-Sinceheader is not even parsed into a date upon HTTP revalidation in Symfony, but the actual string is compared to theLast-Modifiedheader 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-SinceandLast-Modifiedis sufficient and therefore we safely can substitute the custom HTTP revalidation code with Symfonys Response::isNotModified() (and fix our tests accordingly).Comment #2
Crell commentedIf 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.
Comment #3
znerol commentedThere is no reason to be concerned. The Symfony mechanism is perfect, supporting pre-HTTP/1.1 date formats in conditional requests is not necessary.
Comment #4
wim leersWow, #1 is super, super interesting to read. Thanks for that :)
Comment #5
catch#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.
Comment #6
znerol commentedComment #7
znerol commentedTime 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
ETagneeds to fulfill (apart from it being syntactically correct) is that it has to change whenever the underlying resource changes.Comment #8
damien tournoud commentedThis the definition of a strong validator:
(emphasis mine)
Our time-based validator is a strong validator (at least with one-second granularity).
Comment #9
znerol commentedScroll down a little bit and there is:
Comment #10
catchWe can easily fix that particular issue by using microtime() per #5.
Comment #11
damien tournoud commentedMy point is, you don't need a strong validator to be idempotent. you only need it to change when the content changes.
Comment #12
znerol commentedThis is exactly what
somehash($content);does with the additional benefit that we do not depend on (micro)time. Having a stable algorithm to generate theETagalso helps with testing.What exactly is it that makes you think having time in the
ETagis better than supplying a hash?Comment #13
Crell commentedRemember 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.
Comment #14
catchIt 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.
Comment #21
armrus commentedSee the https://tools.ietf.org/html/rfc7232#section-3.3
But https://tools.ietf.org/html/rfc2616##section-13.3.4 says
Correct revalidation implemented in Symfony\Component\HttpFoundation\Response::isNotModified().
I try to use it,
There is Patch.
Comment #22
armrus commentedComment #24
armrus commentedFix tests for new logic of cache checks.
Comment #25
armrus commentedFix tests.
Comment #26
armrus commentedtry to fix login filed test.
Comment #30
it-cruPatch 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.
Comment #31
abhisekmazumdarUpdated the patch for 9.2.x
Comment #33
anmolgoyal74 commentedComment #35
volkerk commentedThere are two additional issues:
The RFC 7232 states in Section 3.2 that
and in Section 3.3
This should be fixed upstream, I will try to get https://github.com/symfony/symfony/issues/37948 re-opened and provide a PR there.
Comment #37
volkerk commentedPR https://github.com/symfony/symfony/pull/40545 was merged, symfony behavior is now correct.
Comment #38
tobiasbSo the patch also needs symfony/http-foundation v4.4.30?
Comment #40
chr.fritschYes, and drupal 9.3.x ships with "v4.4.34"
Comment #43
znerol commentedThanks!
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:
Otherwise this is RTBC.
Comment #44
znerol commentedCR Draft here: https://www.drupal.org/node/3328834
Comment #45
znerol commentedTagging this for global contribution weekend 2023. I hope that somebody likes to pick this up. I'll stick around for questions / reviews.
Comment #46
gambrySwapping the Drupal Global Contribution Weekend 2023 tag with the correct one.
Comment #48
hmdnawaz commentedPatch for Drupal 10.3