Problem/Motivation
Since #3493858: Extend ViewsBlockBase to merge cache metadata from display handler, ViewsBlockBase::getCacheTags() merges the full cache metadata of the display handler (DisplayPluginBase::getCacheMetadata()). ViewsExposedFilterBlock extends ViewsBlockBase and does not override getCacheTags(), so the exposed form block carries every tag of the view results: the query plugin tags, the cache plugin tags and the list cache tags of the base entity type.
The exposed form does not render any result, but it is invalidated as if it did. Exposed forms are often placed in a global region (a header search box), so the result tags end up on every page of the site:
- A view on node_field_data adds node_list: saving any node, of any bundle, invalidates every page.
- A Search API view adds search_api_list:INDEX_ID (SearchApiQuery::getCacheTags()), which Search API invalidates on every indexing run (Index::indexSpecificItems(), Server::deleteItems()). With "Index items immediately" enabled, every content save invalidates every page.
With a CDN or reverse proxy that purges by cache tag (Fastly, Varnish, Cloudflare), this means a site-wide purge on each editorial save or cron indexing run. On the site where we found it, a header search block made pages with a 1 year TTL get re-rendered at origin dozens of times a day. Removing the tag from that block (see workaround) stopped the purges without any visible change in the form.
Our workaround:
function mymodule_block_build_alter(array $build, BlockPluginInterface $block) {
if ($block->getPluginId() === 'views_exposed_filter_block:VIEW_ID-DISPLAY_ID') {
$build['#cache']['tags'] = array_values(array_diff($build['#cache']['tags'] ?? [], ['search_api_list:INDEX_ID']));
}
}
Steps to reproduce
- Standard install. Enable http.response.debug_cacheability_headers: true in development.services.yml.
- Create a view of Content with a page display at /content-search, an exposed "Title" filter, and "Exposed form in a block: Yes".
- Place the "Exposed form: content_search-page_1" block in the Header region.
- Visit the front page (or any page other than /content-search) as anonymous and check the X-Drupal-Cache-Tags header: it contains node_list.
- Create or update any node, e.g. a Basic page that is not listed anywhere. Every page with the block is invalidated in Page Cache, Dynamic Page Cache and any tag-based reverse proxy.
The same happens with a Search API view: the header contains search_api_list:INDEX_ID and every indexing run invalidates every page.
Proposed resolution
The exposed form block should only carry the cacheability of what it renders: the exposed handlers (filters and sorts, whose options may depend on data, see #2887144), the exposed form plugin and the view config entity. It should not inherit the query plugin and cache plugin tags, which describe the result set.
The same reasoning probably applies to getCacheMaxAge(): a time-based results cache should not limit the max-age of the form.
Remaining tasks
TBD.
User interface changes
None.
API changes
None.
Data model changes
None.
Comments
Comment #2
eduardo morales albertiComment #3
eduardo morales albertiComment #4
eduardo morales albertiComment #5
tunicA very shorty briefing: core doesn't distinguish between query plugins in the exposed filter block and the results, so the cache metadata is the same for the exposed filter and for the results (both add
search_api_list:<index>tag). This is incorrect as the page with the results may change whensearch_api_list:<index>is invalidated (because, well, results my have changed), but the page with the exposed filter doesn't (unless it's also includes results, but in that case the cache metadata should be provided by the result part of the page, not the exposed filter).