We have written some custom blocks that render dynamic content; unfortunately, the caching defaults cache the first render of this block (and don't refresh for anonymous users). Cache control seems to only work for authenticated users. Are we missing something obvious? Is it possible to control anonymous caching on a block (or page) basis?
4 Answers
There are two different page caches.
The Internal Dynamic Page Cache works with all kind of elements like blocks and nodes and is uses the cache tags, cache contexts and cache max-age provided by these elements.
The Internal Page Cache is for complete pages for anonymous users and only uses cache tags.
You can control the page cache for anonymous users with one parameter.
Go to the perfomance configuration:
admin/config/development/performance
And configure the page cache maximum age with the time how long your information is valid. This is also important for proxies and the browser cache, because it would not help to invalidate the drupal cache, if the page is cached in other places. After that time the page expires and should be built again. According to the documentation:
Configuring the internal page cache On the Performance page, you can configure how long browsers and proxies may cache pages; that setting is also respected by the Internal Page Cache module. There is no other configuration.
But this is not true. You should configure this time for proxies and browser cache, but the internal page cache does not respect the time you set there. The only reliable solution I found is to uninstall the module Internal Page Cache or set an expire time in an EventSubscriber (for example 3600 sec in the future):
/src/EventSubscriber/SetExpiresSubscriber.php
<?php
namespace Drupal\mymodule\EventSubscriber;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SetExpiresSubscriber implements EventSubscriberInterface {
public function onResponse(FilterResponseEvent $event) {
$request = $event->getRequest();
$response = $event->getResponse();
if ($event->isMasterRequest()) {
$request_time = $request->server->get('REQUEST_TIME');
$expires_time = (new \Datetime)->setTimestamp($request_time + 3600);
$response->setExpires($expires_time);
}
}
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = ['onResponse'];
return $events;
}
}
mymodule.services.yml
services:
mymodule.setexpires:
class: Drupal\mymodule\EventSubscriber\SetExpiresSubscriber
tags:
- { name: event_subscriber }
-
3You can disable the internal page cache with \Drupal::service('page_cache_kill_switch')->trigger(). Of course, if your block is displayed on all pages then that's kinda pointless.– BerdirMar 31, 2016 at 17:32
-
-
@gavz When using the switch, test with
curl --head
, thatmax-age
is correct inCache-Control:
– 4uk4Apr 1, 2016 at 6:43 -
1You could also use a "RequestPolicyInterface" class to deny caching according to some parameters and the request. See api.drupal.org/api/drupal/… for more information.– RenrhafMar 2, 2017 at 3:14
-
To let the performance page settings control the internal page cache expiration, the service's
onResponse()
method in this example could be updated to use$page_max_age = \Drupal::config('system.performance')->get('cache.page.max_age');
instead of hard-coding the expiration to a fixed number of seconds in the future such as 3600. Nov 17, 2017 at 18:24
Check out Added 'no_cache' route option to mark a route's responses as uncacheable
In your routing.yml file:
some.route:
path: '/some/path'
defaults:
_controller: '\Drupal\Some\Controller::response()'
options:
no_cache: TRUE
-
Is there a way to achieve the same outcome, no_cache: TRUE for a particular path, but via theme-level preprocessing?– owenmckJan 6 at 4:29
If you need the Internal Page Cache enabled (i.e., you can't use Varnish or another memory-based solution), and you simply want Internal Page Cache to respect the time set on at admin/config/development/performance
, you could add your own Event Subscriber that modifies the FinishResponseSubscriber that Drupal provides. You can even use Drupal Console to generate most of the Event Subscriber for you, and then take the other bits from FinishResponseSubscriber. Your result would look something like this:
/**
* Class MyModuleSubscriber.
*
* @package Drupal\my_module
*/
class MyModuleSubscriber implements EventSubscriberInterface {
/**
* Sets extra headers on successful responses.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process.
*/
public function onRespond(FilterResponseEvent $event) {
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
$response = $event->getResponse();
$this->setExpiresNoCache($response);
return;
}
/**
* Disable caching in ancient browsers and for HTTP/1.0 proxies and clients.
*
* 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).
*
* @param \Symfony\Component\HttpFoundation\Response $response
* A response object.
*/
protected function setExpiresNoCache(Response $response) {
$response->setExpires(REQUEST_TIME + 300);
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = array('onRespond');
return $events;
}
}
The above hardcodes the cache lifetime at 300 seconds, but you could easily retrieve the user setting instead.
-
> "You can even use Drupal Console to generate most of the Event Subscriber for you," What do I need for? Modul, Controller? Where to past this codec? Mar 14, 2017 at 19:20
-
Hi @FrankGiesecke, there are a number of ways to get Drupal Console. See drupalconsole.com Mar 15, 2017 at 20:22
-
I think you need to disable the 'Internal Page Cache' module to get cache contexts working.
With 'Internal Page Cache' enabled content for anonymous users is cached by default.
If you turn off 'Internal Page Cache' you still need to tell Drupal to not cache for anonymous users
$variables['#cache'] = [
'contexts' => [
// The "current user" is used above, which depends on the request,
// so we tell Drupal to vary by the 'user' cache context.
'user',
],
];
does it for me - but is probably not sompletely correct.