Make ViewAjaxResponse cacheable

Created on 26 February 2025, 4 months ago

Problem/Motivation

AJAX responses from views are never cached by reverse-proxies like Varnish. That's because ViewAjaxResponse does not extend CacheableAjaxResponse so it has no cacheability metadata.

Now that AJAX uses GET requests ( πŸ“Œ Allow AJAX to use GET requests Fixed ) it should be possible to make ViewAjaxController return a cacheable response.

Steps to reproduce

  1. Create an AJAX view with multiple pages.
  2. Enable http.response.debug_cacheability_headers.
  3. Trigger an AJAX request by using the pager.
  4. Inspect the AJAX response headers.
    It has these headers:
    x-drupal-cache: UNCACHEABLE (no cacheability)
    x-drupal-dynamic-cache: UNCACHEABLE (no cacheability)
    

Proposed resolution

ViewAjaxResponse should extend CacheableAjaxResponse.
And ViewAjaxController should apply the cache metadata from the view to the response.

The views_ajax_get module does this and could be used as inspiration: https://git.drupalcode.org/project/views_ajax_get/-/blob/f54e8f83db62dda...

I also see some work was started here: https://www.drupal.org/project/drupal/issues/2500313#comment-12294510 πŸ“Œ Add views render caching on views ajax requests Closed: outdated

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

✨ Feature request
Status

Active

Version

11.1 πŸ”₯

Component

views.module

Created by

πŸ‡«πŸ‡·France prudloff Lille

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

Merge Requests

Comments & Activities

  • Issue created by @prudloff
  • πŸ‡³πŸ‡ΏNew Zealand quietone

    Changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies β†’ .

  • Merge request !11403Make ViewAjaxResponse cacheable β†’ (Open) created by prudloff
  • Pipeline finished with Success
    4 months ago
    Total: 881s
    #442259
  • πŸ‡«πŸ‡·France prudloff Lille
  • πŸ‡«πŸ‡·France prudloff Lille
  • πŸ‡¬πŸ‡§United Kingdom catch

    This looks great to me.

  • πŸ‡§πŸ‡ͺBelgium borisson_ Mechelen, πŸ‡§πŸ‡ͺ

    CR that's added looks great and has all the correct info, RTBC+1

  • πŸ‡¬πŸ‡§United Kingdom catch

    Thanks for the additional reviews and CR. Committed/pushed to 11.x, thanks!

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan
    • catch β†’ committed bf9dd939 on 11.x
      Issue #3509179 by prudloff, nicxvan: Make ViewAjaxResponse cacheable
      
  • πŸ‡¬πŸ‡§United Kingdom catch
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    Updating credit thanks!

  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Entity browser and our project tests break completely with this, something here isn't correct.

    Looking at the response, it has no cacheability metadata at all. $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($preview)); isn't working, $preview not yet rendered at this point, the cacheability metadata is in $preview['view'] and not picked up by createFromRenderArray().

    IMHO, this shouldn't rely on the rendering to set cacheability metadata on there IMHO. The controller implements and uses specific query parameters, such as view_name and view_display_id. It should IMHO always explicitly add those specific query parameters or just url.query_args to the response, otherwise there might be edge cases that might be missing them, for example a weird display that outputs something unexpected?

  • πŸ‡¬πŸ‡§United Kingdom catch

    We should probably roll this back and recommit with that fixed. Not at computer but re-opening so it doesn't get lost.

  • πŸ‡«πŸ‡·France prudloff Lille

    Sorry, I always forget that createFromRenderArray() only takes the root #cache and does not bubble lower cache metadata.

    We can probably use ViewExecutable::getCacheTags() for cache tags but we could also need to get cache contexts and max age from the rendered view.
    The views_ajax_get module uses createFromRenderArray($view->element), I wonder if that's enough.

    @berdir you are right, we should also add cache contexts for query parameters that are explicitly used in the controller.

    • catch β†’ committed d81484de on 11.x
      Revert "Issue #3509179 by prudloff, nicxvan: Make ViewAjaxResponse...
  • πŸ‡¬πŸ‡§United Kingdom catch

    Reverted from 11.x for now.

  • Status changed to Needs work 2 months ago
  • πŸ‡³πŸ‡ΏNew Zealand quietone

    I unpublished the change record.

  • Pipeline finished with Failed
    about 2 months ago
    Total: 96s
    #498970
  • Pipeline finished with Failed
    about 2 months ago
    Total: 98s
    #498981
  • Pipeline finished with Success
    about 2 months ago
    Total: 640s
    #498994
  • πŸ‡«πŸ‡·France prudloff Lille

    Looking at the response, it has no cacheability metadata at all. $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($preview)); isn't working, $preview not yet rendered at this point, the cacheability metadata is in $preview['view'] and not picked up by createFromRenderArray().

    I can't reproduce this on the views I'm testing with, $preview['#cache'] is full when I inspect it. But maybe it depends on the view?

    Anyway I updated the MR to use the same method as the views_ajax_get module, which might be more robust.
    The test_ajax_view view does not have a lot of cache metadata so I also used a node base view in the test to get more realistic cache tags like node_list.
    I also tested manually on a real view and the metadata seemed correct

    Instead of url.query_args, we could use more specific cache contexts, but the controller uses a lot of different query arguments so I'm not sure that's worth it.

    @berdir it would be great if you could test your use case.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Sure we want to change to change ViewAjaxTest to use Nodebase? Doesn't that make node a hard dependency in view

  • Status changed to Needs review 1 day ago
  • πŸ‡·πŸ‡ΊRussia qzmenko Novosibirsk

    I'm experiencing issue when browser cache is disabled (e.g. using "Disable cache" in Chrome DevTools). The first AJAX request works fine, but the second one fails with the following error:

    Error: Call to a member function get() on null Π² Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers() (строка 867 ΠΈΠ· /var/www/html/web/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php)
    #0 /var/www/html/web/core/modules/views/src/ViewExecutable.php(1100): Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('field')
    #1 /var/www/html/web/core/modules/views/src/ViewExecutable.php(958): Drupal\views\ViewExecutable->_initHandler('field', Array)
    #2 /var/www/html/web/core/modules/views/src/ViewExecutable.php(2586): Drupal\views\ViewExecutable->initHandlers()
    #3 [internal function]: Drupal\views\ViewExecutable->__wakeup()
    #4 /var/www/html/web/core/lib/Drupal/Component/Serialization/PhpSerialize.php(21): unserialize('O:34:"Drupal\\vi...')
    #5 /var/www/html/web/modules/contrib/redis/src/Cache/CacheBase.php(381): Drupal\Component\Serialization\PhpSerialize::decode('O:34:"Drupal\\vi...')
    #6 /var/www/html/web/modules/contrib/redis/src/Cache/Predis.php(69): Drupal\redis\Cache\CacheBase->expandEntry(Array, false)
    #7 /var/www/html/web/modules/contrib/redis/src/Cache/CacheBase.php(159): Drupal\redis\Cache\Predis->getMultiple(Array, false)
    #8 /var/www/html/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(319): Drupal\redis\Cache\CacheBase->get('http://vitrina....', false)
    #9 /var/www/html/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(134): Drupal\page_cache\StackMiddleware\PageCache->get(Object(Symfony\Component\HttpFoundation\Request))
    #10 /var/www/html/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(87): Drupal\page_cache\StackMiddleware\PageCache->lookup(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #11 /var/www/html/web/core/modules/ban/src/BanMiddleware.php(50): Drupal\page_cache\StackMiddleware\PageCache->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #12 /var/www/html/web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(48): Drupal\ban\BanMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #13 /var/www/html/web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(51): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #14 /var/www/html/web/core/lib/Drupal/Core/StackMiddleware/AjaxPageState.php(36): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #15 /var/www/html/web/core/lib/Drupal/Core/StackMiddleware/StackedHttpKernel.php(51): Drupal\Core\StackMiddleware\AjaxPageState->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #16 /var/www/html/web/core/lib/Drupal/Core/DrupalKernel.php(741): Drupal\Core\StackMiddleware\StackedHttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #17 /var/www/html/web/index.php(19): Drupal\Core\DrupalKernel->handle(Object(Symfony\Component\HttpFoundation\Request))
    #18 {main}
Production build 0.71.5 2024