Problem/Motivation
This was extracted from #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage (this is a critical subset of that issue, which is blocking #2765959).
In \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber::on4xx()
, we are returning a \Symfony\Component\HttpFoundation\Response
object. Which does not implement \Drupal\Core\Cache\CacheableResponseInterface
, and hence prevents cacheability metadata (tags, contexts, max-age) from being added to the response.
Consequently, page_cache
and dynamic_page_cache
will not cache these 4xx responses.
This is for 4xx responses though, so why do we care?
Turns out there are use cases where we want cached 4xx responses — for example: some 4xx responses are requested many times (and may also be very expensive).
Proposed resolution
When we are faced with a client error, we should return a \Drupal\Core\Cache\CacheableResponse
. Then \Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber
will automatically add the 4xx-response
cache tag, just like for HTML responses.
But in order for error responses to be CacheableResponse
and not just Response
, we of course need to know how to cache an error response. Symfony's \Symfony\Component\HttpKernel\Exception\HttpException(Interface)
, which we use to do throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException($message)
, of course does not carry/provide cacheability information at all.
This issue proposes to solve that by introducing \Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException
and friends, which provide "cacheability-capable" subclasses of all Symfony exception classes, like this:
class CacheableAccessDeniedHttpException extends AccessDeniedHttpException implements CacheableDependencyInterface {
use CacheableDependencyTrait;
}
In other words: provide "richer" versions of the existing exceptions. Code that actually throws those richer exceptions can then actually have cacheable error responses. All other error responses remain not cacheable. Therefore there is no BC break; this is purely opt-in.
Remaining tasks
None.
User interface changes
None.
API changes
API addition: CacheableHttpExceptionInterface
. Exceptions of this type are cacheable, and will therefore generate cacheable 4xx responses that can be cached by Page Cache & Dynamic Page Cache.
Data model changes
None.
Comment | File | Size | Author |
---|---|---|---|
#25 | 2920001-25.patch | 35.93 KB | Wim Leers |
Comments
Comment #2
Wim LeersFirst, let's import the relevant parts of the IS from the parent issue.
Comment #3
Wim LeersFirst: the foundations!
CacheableAccessDeniedHttpException
+CacheableBadRequestHttpException.php
and so onCacheableDependencyTrait
extracted fromRefinableCacheableDependencyTrait
, which the above can useDefaultExceptionHtmlSubscriber
andExceptionJsonSubscriber
to respect cacheability metadata and bubble it up to the responses they generateImported straight from the parent issue's patch.
Comment #4
Wim LeersNext: the first use case!
This updates
\Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
to throw a cacheable HTtP exception rather than an uncacheable one.Imported straight from the parent issue's patch.
Comment #5
Wim LeersNext: explicit test coverage for the first use case.
This provides explicit functional test coverage for the changes in #4, by adding
\Drupal\Tests\basic_auth\Functional\BasicAuthTest::testCacheabilityOf401Response()
.This was not imported from the parent issue's patch.
Comment #6
Wim LeersNext: fixing the failing tests.
We're getting a 401 where a 403 is expected now. Turns out that the "REST test" authentication provider simply was buggy: it did not yet support Page Cache correctly. This simply went unnoticed until now because the REST test coverage doesn't test Page Cache support yet for every request it makes, and it should. (But doing that is in the scope of the parent issue, not this issue.)
Imported straight from the parent issue's patch.
Comment #7
Wim LeersNext: fixing the remaining failing tests.
#6 fixed some of the failing tests: we're able to get further along in those test cases, but we're still failing. This is because Dynamic Page Cache is now caching more than only a 200 response when testing with Basic Auth: it's also caching a 4xx response. We simply need to update the expectations.
This should be green again.
Imported straight from the parent issue's patch.
Comment #11
Wim LeersYay, green, as predicted :)
Note we should credit @davidwbarratt and @dawehner — they've been huge influences in this patch, in the parent issue.
Also, the parent issue is now officially postponed on this issue: see #2765959-230: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage.
Comment #12
borisson_I was ready to RTBC the other issue, and this one is smaller - all the nitpicks I had were resolved in the other issue.
Comment #13
aheimlichI've seen
Drupal\Core\Cache\Exception\CacheableHttpExceptionInterface
mentioned several times, yet I don't see any code for it. None of the exceptions added in this patch are implementing it, either. Am I missing something?Comment #14
dawehnerIt would be nice to have an exception extending
HttpException
itself.Comment #15
Wim Leers#12: ❤️
#13: that's a good catch: that's indeed no longer relevant, it's
CacheableDependencyInterface
that matters now!#14: Sure, I can add that too.
#13 + #14 are excellent nits, thanks!
Comment #16
Wim LeersDone!
Comment #17
dawehnerThank you @Wim Leers!
Comment #19
Wim LeersComment #20
effulgentsia CreditAttribution: effulgentsia at Acquia commentedTicking credit boxes for all the reviewers. Thank you.
Comment #22
effulgentsia CreditAttribution: effulgentsia at Acquia commentedAlso crediting @davidwbarratt per #11.
Comment #23
effulgentsia CreditAttribution: effulgentsia at Acquia commentedJust a reroll due to changes committed in #2874938: AdminRouteSubscriber must only mark HTML routes as administrative.
Comment #24
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThis patch looks great to me. Some relatively minor nits/questions:
What about if $exception is not a CacheableDependencyInterface. Do we need code somewhere to make $response not cacheable in such a situation?
This looks great. Shouldn't we also add a test for it?
Our other cache policies (e.g., DisallowBasicAuthRequests) include the reasoning for them in the PHPDoc. Even though this is just a test class, can we still add the reason for having this policy in its docs?
Comment #25
Wim Leers🎉
Note that unlike in
ExceptionJsonSubscriber
, we're not generating a response! We're enriching a response that we receive from a subrequest: we receive the response for one of the\Drupal\system\Controller\Http4xxController()
routes.However, I think you're right: when we deal with a cacheable response for an exception that itself is not cacheable, we should also set it to become uncacheable, by setting
max-age = 0
. Fortunately, doing that is absolutely trivial thanks to the existing cacheability-related infrastructure. So I guess it does make sense to do here after all :)CacheableMethodNotAllowedException
, which we could easily have fixed later,but still, good to have it fixed now!)
Comment #26
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThanks!
Unticking my own credit box, since #23 was just a straight reroll.
Comment #28
effulgentsia CreditAttribution: effulgentsia at Acquia commentedPushed to 8.5.x.
Comment #29
Wim Leers🎉🎉🍾
#2765959 is now fully unblocked: #2765959-232: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage!
Comment #31
Wim Leers#2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage took advantage of the APIs introduced here to bring that to core's REST module. #2929428: [>=8.5] Convert "throw new *HttpException" into "throw new Cacheable*HttpException" where possible aims to do the same for the contrib JSON API module.