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 (#956186: Allow AJAX to use GET requests) it should be possible to make ViewAjaxController return a cacheable response.
Steps to reproduce
- Create an AJAX view with multiple pages.
- Enable http.response.debug_cacheability_headers.
- Trigger an AJAX request by using the pager.
- 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
Remaining tasks
Review
User interface changes
NA
Introduced terminology
NA
API changes
ViewAjaxResponse now extends CacheableAjaxResponse and adds cacheability metadata.
Data model changes
NA
Release notes snippet
ViewAjaxResponse now extends CacheableAjaxResponse and adds cacheability metadata.
| Comment | File | Size | Author |
|---|
Issue fork drupal-3509179
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
quietone commentedChanges are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies.
Comment #4
prudloff commentedComment #5
prudloff commentedComment #6
catchThis looks great to me.
Comment #7
borisson_CR that's added looks great and has all the correct info, RTBC+1
Comment #9
catchThanks for the additional reviews and CR. Committed/pushed to 11.x, thanks!
Comment #12
catchComment #13
nicxvan commentedUpdating credit thanks!
Comment #14
berdirEntity 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?
Comment #15
catchWe should probably roll this back and recommit with that fixed. Not at computer but re-opening so it doesn't get lost.
Comment #16
prudloff commentedSorry, 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.
Comment #18
catchReverted from 11.x for now.
Comment #19
quietone commentedI unpublished the change record.
Comment #21
prudloff commentedI 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.
Comment #22
smustgrave commentedSure we want to change to change ViewAjaxTest to use Nodebase? Doesn't that make node a hard dependency in view
Comment #23
qzmenkoI'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:
Comment #24
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #25
prudloff commentedI merged the latest 11.x.
Comment #26
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #27
prudloff commentedI merged the latest 11.x.
I think the reason was that the test entities don't have a whole lot of cache parameters so using a view with nodes makes it easier to test that it has the correct cache tags like node_list (see #21).
But maybe that's a bad idea.
Comment #28
ressaThanks for working on this @prudloff! It would be really nice to get better Views AJAX caching support.
I guess the expected result after patching is that these:
Change to this?
To make it clear what to check for after patching, could an "Expected result" be added in the Issue Summary?
Comment #29
smustgrave commentedMy comment in #22 may still stand but moving to NW as it has merge conflicts.
Comment #30
harivansh commentedComment #31
harivansh commentedUnable to reproduce the issue #22
Comment #32
harivansh commentedComment #33
smustgrave commentedNR isn’t the correct status
Comment #34
prudloff commentedI removed the dependency on node in ViewAjaxTest.
Comment #35
smustgrave commentedBelieve all feedback has been addressed
Ran the test-only https://git.drupalcode.org/issue/drupal-3509179/-/jobs/7771835 shows the coverage
CR is already written and straight forward.
Tweaked the IS slightly incase this needed a snippet (just copied from the CR)
LGTM
Comment #37
catchOne small comment on the MR, but it confused me reviewing this issue twice.
Comment #38
prudloff commentedI removed the confusing comment.
Comment #39
godotislateI added a couple comments about the
->expects($this->any())added usages here that apparently should be removed per #3561671: [meta] Refactor tests to use stubs instead of mocks where mocks do not configure expectations. Though I don't know that it should be a blocker here as long it gets cleaned up there?Otherwise +1 RTBC.
Comment #40
berdirI think this could benefit from more explicit, real test coverage that caching actually works. At least in our case, there are ... complications. I see that the responses, at least some of them, are being cached, but the query strings are also always different.
I think part of that is conflicts with #2823541: Table clicksort is lost when using views exposed filter & Pager exposed '#items', which we need for our custom block filter display plugin, closely related to #2605218: Views Block Display skips preBlockBuild() call on ajax rebuild in combination with the new setBrowserUrl ajax command. It's quite hard to get into a case where the query strings are the same. But another thing is that the views dom id is being passed around, and that changes every time the view is rendered, so that too complicates cache hits.
After playing around with it a bit, I managed to get a page cache hit, which resulted in a fatal error with this backtrace:
Not sure if this needs specific handlers or displays to trigger, but try adding another request to your test to test the cache hit/miss response headers. Browser testing would also be good I think, but I'm not sure if we can detect in JS tests that it's being cached? maybe if we query the cache tables?
The problem here is that the ViewAjaxResponse object stores the view, and that gets serialized together with the response. I'm not entirely sure where that's even used. I see for example \Drupal\geolocation\EventSubscriber\AjaxResponseSubscriber::onResponse() doing some stuff with ajax responses but nothing in core outside of the unit test? Should we remove the view on serialization of the view?
Comment #41
prudloff commentedIn theory if you have page cache enabled the view page should be cached and the DOM ID should be the same for everyone visiting the page with the exact same URL.
However if you load the page with different GET parameters you will definitely get a different DOM ID and this means cache hits are not optimal (it basically creates a set of AJAX cached entries for each URL that was used to access the view initially) but IMHO improving this would be out of scope for this issue.
I guess we could add a browser test that does something like this:
I tested this scenario manually and indeed got the "Call to a member function get() on null" error.
This means we have steps to reproduce in order to fix it.
I just noticed that the views_ajax_get module (which inspired this MR) does this: https://git.drupalcode.org/project/views_ajax_get/-/blob/f54e8f83db62dda...
So we probably need to do something similar.
Comment #43
duaelfrRerolled on main and added a test on X-Drupal-Cache
Comment #44
duaelfrI don't understand the test failure. Can somebody have a quick look, please?
Comment #46
duaelfrRerolled and fixed tests 🤞
@prudloff was right in #41: we had to implement custom serialization on ViewAjaxResponse like the views_ajax_get module did.
Comment #47
duaelfrPreviously failing tests are now green!
Comment #48
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. The merge request has merge conflicts and cannot be merged. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #49
duaelfrRerolled to fix the conflict