The no_cache" route option is required even on a response with "cache max age" set to 0

Created on 15 September 2021, almost 4 years ago
Updated 27 October 2023, over 1 year ago

Problem/Motivation

We are using the JSON:API Resources module to define a custom JSON:API endpoint. We are setting the cacheability metadata on the resource being returned so that the max-age is set to 0. Despite this, when requests to the endpoint are accessed by anonymous users, the response is returned from the page cache unless we add the "no_cache" option to the route in routing.yml. Shouldn't the fact that the cacheability is set to a max age of 0 bubble-up to the page cache determination?

Steps to reproduce

  1. Install JSON:API Resources β†’ .
  2. Create a custom module that defines a custom JSON:API resource endpoint, following examples from the module. For example:
    
    namespace Drupal\bug_repro\Resource;
    
    use Drupal\Component\Datetime\TimeInterface;
    use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
    use Drupal\Component\Plugin\Exception\PluginNotFoundException;
    use Drupal\Core\Cache\CacheableMetadata;
    use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
    use Drupal\jsonapi\JsonApiResource\LinkCollection;
    use Drupal\jsonapi\JsonApiResource\ResourceObject;
    use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
    use Drupal\jsonapi\ResourceResponse;
    use Drupal\jsonapi\ResourceType\ResourceType;
    use Drupal\jsonapi_resources\Resource\ResourceBase;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Routing\Route;
    
    class BugReproResource extends ResourceBase implements ContainerInjectionInterface {
    
      /**
       * The Drupal time service.
       *
       * @var \Drupal\Component\Datetime\TimeInterface
       */
      protected $timeService;
    
      /**
       * {@inheritDoc}
       */
      public static function create(ContainerInterface $container): BugReproResource {
        return new static($container->get('datetime.time'));
      }
    
      /**
       * Constructor for BugReproResource.
       *
       * @param \Drupal\Component\Datetime\TimeInterface $time_service
       *   The Drupal time service.
       */
      public function __construct(TimeInterface $time_service) {
        $this->timeService = $time_service;
      }
    
      /**
       * Process an bug repro query.
       *
       * @param \Symfony\Component\HttpFoundation\Request $request
       *   The request.
       *
       * @return \Drupal\jsonapi\ResourceResponse
       *   The response.
       */
      public function process(Request $request): ResourceResponse {
        $resource_type = new ResourceType('bug_repro', 'bug_repro', NULL);
    
        // Never cache.
        $cacheability =
          (new CacheableMetadata())
            ->setCacheMaxAge(0)
            ->addCacheContexts(['url.path', 'url.query_args']);
    
        $attributes = ['timestamp' => $this->getTimeService()->getCurrentTime()];
    
        $primary_data =
          new ResourceObject(
            $cacheability,
            $resource_type,
            'fake',
            NULL,
            $attributes,
            new LinkCollection([])
          );
    
        $top_level_data = new ResourceObjectData([$primary_data], 1);
    
        try {
          $response = $this->createJsonapiResponse($top_level_data, $request);
        }
        catch (InvalidPluginDefinitionException | PluginNotFoundException $ex) {
          throw new \RuntimeException(
            'Failed to create JSON:API response: ' . $ex->getMessage(),
            0,
            $ex
          );
        }
    
        $response->addCacheableDependency($cacheability);
    
        return $response;
      }
    
      /**
       * Gets the Drupal time service.
       *
       * @return \Drupal\Component\Datetime\TimeInterface
       *   The interface for getting access to request time.
       */
      protected function getTimeService(): TimeInterface {
        return $this->timeService;
      }
    
      /**
       * {@inheritdoc}
       */
      public function getRouteResourceTypes(Route $route,
                                            string $route_name): array {
        $resource_type =
          new ResourceType(
            'bug_repro',
            'bug_repro',
            NULL,
            FALSE,
            TRUE,
            FALSE,
            FALSE,
            []
          );
    
        return [$resource_type];
      }
    
    }
    
  3. Ensure that routing in the custom module is setup for the resource:
    bug_repro:
      path: '/%jsonapi%/bug_repro'
      defaults:
        _jsonapi_resource: '\Drupal\example\Resource\BugReproResource'
      requirements:
        _access: 'TRUE'
    
  4. Clear site caches.
  5. In a private browsing window, request /jsonapi/bug_repro.
  6. Over a span of about five seconds, keep refreshing the page, paying attention to the timestamp attribute returned in the resource.
  7. Now, modify the routing to add the no_cache option, like so:
  8. Ensure that routing in the custom module is setup for the resource:
    bug_repro:
      path: '/%jsonapi%/bug_repro'
      defaults:
        _jsonapi_resource: '\Drupal\example\Resource\BugReproResource'
      requirements:
        _access: 'TRUE'
      options:
        no_cache: TRUE
    
  9. Clear site caches.
  10. In a private browsing window, request /jsonapi/bug_repro.
  11. Over a span of about five seconds, keep refreshing the page, paying attention to the timestamp attribute returned in the resource.

You will notice that the timestamp does not increment unless the route is set not to cache, even though the cacheability of the response is set to not cache.

Proposed resolution

If any element of a page indicates a cache max age of 0, the page should not be cacheable.

Remaining tasks

Either:

  • Add a response cache policy that respects content cacheability;
    OR
  • Correct cacheability logic in \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond() to properly detect bubbling of cacheability from page content.

User interface changes

API changes

Data model changes

Release notes snippet

πŸ› Bug report
Status

Postponed: needs info

Version

11.0 πŸ”₯

Component
CacheΒ  β†’

Last updated 12 days ago

Created by

πŸ‡ΊπŸ‡ΈUnited States GuyPaddock

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

Production build 0.71.5 2024