Problem
ESI fragments are never held by Varnish, and even if they were, a change to the content a block shows could not invalidate them. The two faults hide each other: because nothing is cached, the missing tags are invisible.
1. Fragments are refused by the VCL
The ESI callbacks are declared with no_cache, so Drupal marks every fragment private. The shipped VCL has a rule meant to let such a response through:
elsif (beresp.http.Cache-Control ~ "private" && !beresp.http.X-DOESI) {
A fragment never carries X-DOESI. CacheManager::esiEnabled() returns FALSE for ESI requests by design, since the flag means "this response contains esi:include tags", and a fragment contains none. So the fragment lands in that branch and is marked uncacheable. Measured through a live Varnish:
page cache-control: max-age=3600, public x-doesi: 1 x-cacheable: YES
fragment cache-control: must-revalidate, no-cache, private (no x-doesi)
x-ttl: 120 x-cacheable: NO:Cache-Control=private
x-varnish-cache: MISS MISS MISS2. Excepting the ESI path is not enough on its own
sub vcl_backend_response does not end in a return, so the built in rules run after it and refuse anything carrying no-cache, no-store or private regardless of what the module decided. The !X-DOESI clause has therefore never had any effect.
Returning unconditionally is not the answer. Once ESI is on, an authenticated response carries X-DOESI too, so the module calls it cacheable, and a redirect meant for one visitor ends up in a bin shared by everyone holding the same roles. This was reproduced: a second user was served the first user's profile redirect.
3. Fragments carry none of the tags of what they show
ESIBlockController::content() declares its dependencies before rendering and renders with renderInIsolation(), which deliberately keeps the metadata out of the surrounding render context and collects it into the build array instead. That array is never read back, so everything that surfaces while the block renders is dropped.
A fragment ships with config:block_list, http_response and the role tag, and nothing else. No node_list, no node:N. An editor saving content has nothing to invalidate the fragment by.
4. A stale bin cookie turns a fragment into a redirect
A visitor whose ADVBIN cookie does not match is answered by setting the cookie and redirecting. On an ESI callback that replaces the block with the empty body of the redirect. This is what #3072725: Avoid redirect for anonymous on first visit addressed for the page, and it is still reachable on the callbacks. A log out, a change of roles or a change of the hashing noise all produce a stale cookie.
Proposed resolution
- Except only
/adv_varnish/esi/block/from the private rule, refuseno-storeahead of it, and return early for that route alone, on a plain 200. The status matters: the module puts its own lifetime on anything that is not anESIResponse, so an error on that path would otherwise be held for the full TTL. - Leave the other ESI route alone.
user_blockscarries the personal data that has to be fresh on every page, and it must keep being refused. - Collect the cache metadata from the build after rendering in
ESIBlockController. - Skip the reload on ESI callbacks. The cookie is still set, and the request for the page itself carries out the reload.
Why this is one issue
The three fixes are not independent. Caching fragments without collecting their tags would serve stale content, and doing it without the redirect fix would let a redirect be assembled into the page. Split up, each part lands a change that is worse than the current state.
Issue fork adv_varnish-3620917
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:
Comments
Comment #4
shumer commented