3

I've created a custom cache context that determines if a request comes from in my campus network or from outside my campus network:

class NetworkContext extends RequestStackCacheContextBase implements CacheContextInterface {

  /**
   * {@inheritdoc}
   */
  public static function getLabel() {
    return t('Network');
  }

  /**
   * {@inheritdoc}
   */
  public function getContext() {
    if (Util::requestFromCampus($this->requestStack->getCurrentRequest()->getClientIp())) {
      return 'oncampus';
    }
    else {
      return 'offcampus';
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheableMetadata() {
    return new CacheableMetadata();
  }

}

This custom cache context is registered in my module's services file and given the name "network".

I have a custom controller action that returns some data that varies based on if the user is on campus or off campus:

public function myCustomAction(Request $request) {
  $build = [];

  $build['my_data'] = [
    '#theme' => 'my_custom_theme_function',
    '#rawdata' => $this->fetchData(Util::requestIsFromCampus($request->getClientIp()));
  ];

  // Add some cache metadata that indicates this render array varies
  // based on the user's network. We can't effectively use cache tags
  // here since the data is retrieved from an external service and we
  // don't know when it's updated. Instead, expire the result after
  // one hour.
  $build['#cache'] = [
    'max-age' => 60 * 60,
    'contexts' => [
      'network',
    ],
  ];

  return $build;
}

Here's what I've found:

Internal Page Cache won't work here: The Internal Page Cache module, to my knowledge, doesn't support cache contexts at all. It will store the entire page result for ALL anonymous traffic. If the request that warmed the cache for this page happened to be on campus, off campus users will also see that result, despite their cache context being different. This means I have to tell Internal Page Cache to not cache this page. One way is to add the "no_cache" option to the routing definition, which is what I've done.

Dynamic Page Cache DOES work, but not when I have the "no_cache" option set. I've found that dynamic page cache DOES support cache contexts entirely, so it will correctly vary the response by my custom cache tag! However, I can't keep Dynamic Page Cache working while also turning off Internal Page Cache, because the mechanism to disable one will disable both.

My questions:

  1. Is there a way I can disable internal page cache for a page but NOT dynamic page cache?
  2. Is using the dynamic page cache appropriate here, or am I using it incorrectly? I looked into auto placeholdering but that seems to be for things that are highly dynamic, but my cache context is not. It's either "yes" or "no".

3 Answers 3

2

Page cache is a service, which means you can override it in a ServiceProvider of a custom module, subclass it and override \Drupal\page_cache\StackMiddleware\PageCache::getCacheId(), where you can make the IP subnet or what you need exactly part of the cache key.

You could simply disable the page_cache module and still use the dynamic page cache.

Yet another option would be a to implement a request policy that prevents caching either for internal or external requests, I think dynamic page cache doesn't consider that. See \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests for an example.

3
  • Interesting idea about overriding the service and providing a different CID! I hadn't even thought of an approach like that. D8 win. For your last suggestion, Dynamic Page Cache uses the request policy service as well, so it wouldn't work.
    – Brian
    Apr 13, 2017 at 13:51
  • I just revisited this, and I think that overriding the internal page cache service to change the CID won't work if you're also using varnish... since Varnish has its own cache ID based on the URL only, right?
    – Brian
    Sep 27, 2017 at 15:09
  • 1
    Varnish is based on whatever you configure it to be based on, it's pretty flexible, you should be able to either bypass cache for certain IP's or even consider that for the cache key. But I don't know the details, there are better places for asking varnish specific questions
    – Berdir
    Sep 27, 2017 at 20:38
2

One approach could be to use a middleware to set a query parameter:

  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
    $ip = $request->getClientIp();
    if (Util::requestFromCampus($ip)) {
      $request->query->add(['oncampus' => '1']);
    }
    else {
      // remove the query parameter in case an off-campus user sets it incorrectly
      $request->query->remove('oncampus');  
    }
    return $this->httpKernel->handle($request, $type, $catch);
  }

Set a priority higher than 200, so that it runs before the page cache. The page cache uses the url including the query string as cache key and so both versions can be delivered from cache, which is good for performance.

As example how to set this up see Drupal\ban\BanMiddleware.

If you use this approach you don't need the custom cache context anymore, you can use a context for the query argument in content that varies for off- and on-campus users.

1
  • This is very interesting, but I don't like the potential implications of artificially injecting a query string parameter like that.
    – Brian
    Apr 13, 2017 at 13:53
0

I found that the core maintenance mode module makes a call to the service \Drupal::service('page_cache_kill_switch')->trigger(); in MaintenanceModeSubscriber.php so that the maintenance mode page does not get cached.

I was able to add this to my custom Event Subscriber:

<?php

namespace Drupal\my_module\EventSubscriber;

/**
 * Class MyCustomSubscriber.
 */
class MyCustomSubscriber implements EventSubscriberInterface {

  public static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST] = ['myCustomRedirect', 28];
    return $events;
  }

  public function myCustomRedirect(GetResponseEvent $response, $event, ContainerAwareEventDispatcher $event_dispatcher) {
    if (\Drupal::service('router.admin_context')->isAdminRoute()) {
      return;
    }

    \Drupal::service('page_cache_kill_switch')->trigger();

    // ...

  }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.