Problem/Motivation

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:

  1. If you have a use case that is targeted at anonymous users/clients — imagine a huge number of requests, and re-computing the same 404 or 403 response tens of thousands of times. This has the potential to bring down servers. Having a cached response would be much faster & cheaper.
  2. If you have an entity with many entity reference values, the application consuming the entity must go through the references to get the referenced data, if some of these items are inaccessible (or unpublished) then it will return a 403. Having that 403 not cached will be again be very expensive and slow.
  3. … and others.

An additional benefit of having this be supported would be that we can improve the test coverage of cacheability metadata for REST resource plugins (and the EntityResource in particular).

Proposed resolution

  1. Add support for cacheable HTTP exceptions: #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata
  2. Update EntityResource::get() to use this
  3. Update (Entity)ResourceTestBase's test coverage to ensure that we explicitly test every response during REST tests for its Page Cache & Dynamic Page Cache support

Remaining tasks

  1. Patch
  2. Tests
  3. Make scope of this patch more manageable by landing #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata, which was extracted from here
  4. Land this patch, which is now more manageable

User interface changes

None.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
#246 rest_cacheable_response-2765959-246.patch51.59 KBWim Leers
#246 interdiff.txt2.42 KBWim Leers
#244 rest_cacheable_response-2765959-244.patch49.25 KBWim Leers
#244 interdiff.txt1.29 KBWim Leers
#240 rest_cacheable_response-2765959-240.patch48.02 KBWim Leers
#240 interdiff.txt835 bytesWim Leers
#238 rest_cacheable_response-2765959-238.patch48.02 KBWim Leers
#238 interdiff.txt2.64 KBWim Leers
#235 rest_cacheable_response-2765959-235.patch46.44 KBWim Leers
#232 rest_cacheable_response-2765959-232.patch45.58 KBWim Leers
#231 rest_cacheable_response-2765959-231.patch72.08 KBWim Leers
#224 rest_cacheable_response-2765959-224.patch77.62 KBWim Leers
#224 interdiff.txt11.95 KBWim Leers
#222 rest_cacheable_response-2765959-222.patch77.66 KBWim Leers
#222 interdiff.txt31.62 KBWim Leers
#221 rest_cacheable_response-2765959-221.patch73.79 KBWim Leers
#221 interdiff.txt1.93 KBWim Leers
#219 rest_cacheable_response-2765959-219.patch71.91 KBWim Leers
#217 rest_cacheable_response-2765959-217.patch72.05 KBWim Leers
#217 interdiff.txt6.82 KBWim Leers
#207 rest_cacheable_response-2765959-207.patch65.68 KBWim Leers
#207 interdiff.txt18.28 KBWim Leers
#205 rest_cacheable_response-2765959-205.patch65.75 KBWim Leers
#205 interdiff.txt4.58 KBWim Leers
#202 interdiff.txt2.42 KBWim Leers
#202 rest_cacheable_response-2765959-202.patch66.96 KBWim Leers
#198 rest_cacheable_response-2765959-198.patch67.19 KBWim Leers
#198 interdiff.txt2.73 KBWim Leers
#195 rest_cacheable_response-2765959-195.patch67.19 KBWim Leers
#195 interdiff.txt1.56 KBWim Leers
#194 rest_cacheable_response-2765959-194.patch66.13 KBWim Leers
#194 interdiff.txt2.03 KBWim Leers
#193 rest_cacheable_response-2765959-193.patch65.84 KBWim Leers
#191 rest_cacheable_response-2765959-191.patch65.18 KBWim Leers
#191 interdiff.txt1.64 KBWim Leers
#188 rest_cacheable_response-2765959-188.patch65.25 KBWim Leers
#188 rebase-interdiff.txt6.78 KBWim Leers
#188 interdiff.txt10.76 KBWim Leers
#184 rest_cacheable_response-2765959-184.patch59.5 KBWim Leers
#184 interdiff.txt2.73 KBWim Leers
#183 rest_cacheable_response-2765959-183.patch57.3 KBWim Leers
#183 interdiff.txt2.45 KBWim Leers
#182 rest_cacheable_response-2765959-182.patch56.69 KBWim Leers
#182 interdiff.txt1.84 KBWim Leers
#181 rest_cacheable_response-2765959-181.patch54.92 KBWim Leers
#181 interdiff.txt2.58 KBWim Leers
#180 rest_cacheable_response-2765959-180.patch52.84 KBWim Leers
#180 interdiff.txt3.87 KBWim Leers
#179 rest_cacheable_response-2765959-179.patch50.67 KBWim Leers
#179 interdiff.txt9.99 KBWim Leers
#178 rest_cacheable_response-2765959-178.patch44.5 KBWim Leers
#178 interdiff.txt4.16 KBWim Leers
#177 rest_cacheable_response-2765959-177.patch40.83 KBWim Leers
#177 interdiff.txt9.28 KBWim Leers
#176 rest_cacheable_response-2765959-176.patch33.9 KBWim Leers
#176 interdiff.txt5.83 KBWim Leers
#171 rest_cacheable_response-2765959-171.patch28.2 KBWim Leers
#171 interdiff.txt1.52 KBWim Leers
#170 rest_cacheable_response-2765959-170.patch29.65 KBWim Leers
#170 interdiff.txt1.93 KBWim Leers
#169 rest_cacheable_response-2765959-169.patch31.52 KBWim Leers
#169 interdiff.txt5.57 KBWim Leers
#167 rest_cacheable_response-2765959-167.patch30.97 KBWim Leers
#167 interdiff.txt868 bytesWim Leers
#160 rest_cacheable_response-2765959-160.patch31.03 KBWim Leers
#160 interdiff.txt21.87 KBWim Leers
#158 rest_cacheable_response-2765959-158.patch33.46 KBWim Leers
#158 interdiff.txt898 bytesWim Leers
#155 rest_cacheable_response-2765959-155.patch32.62 KBWim Leers
#155 interdiff.txt1.12 KBWim Leers
#151 rest_cacheable_response-2765959-151.patch31.99 KBWim Leers
#151 interdiff.txt1.97 KBWim Leers
#149 rest_cacheable_response-2765959-149.patch30.11 KBWim Leers
#145 rest_cacheable_response-2765959-145.patch34.81 KBdavidwbarratt
#145 inner-diff.txt720 bytesdavidwbarratt
#142 inner-diff.txt1.04 KBdavidwbarratt
#142 rest_cacheable_response-2765959-142.patch35.51 KBdavidwbarratt
#141 inner-diff.txt1.37 KBdavidwbarratt
#141 rest_cacheable_response-2765959-141.patch36.55 KBdavidwbarratt
#139 rest_cacheable_response-2765959-139.patch37.92 KBdavidwbarratt
#139 inner-diff.txt1.24 KBdavidwbarratt
#137 inner-diff.txt911 bytesdavidwbarratt
#137 rest_cacheable_response-2765959-137.patch39.16 KBdavidwbarratt
#135 inner-diff.txt932 bytesdavidwbarratt
#135 rest_cacheable_response-2765959-135.patch38.63 KBdavidwbarratt
#133 rest_cacheable_response-2765959-133.patch37.72 KBdavidwbarratt
#131 inner-diff.txt685 bytesdavidwbarratt
#131 rest_cacheable_response-2765959-131.patch39.5 KBdavidwbarratt
#129 rest_cacheable_response-2765959-129.patch39.5 KBdavidwbarratt
#127 rest_cacheable_response-2765959-127.patch42.06 KBdavidwbarratt
#125 inner-diff.txt3.31 KBdavidwbarratt
#125 rest_cacheable_response-2765959-125.patch42.15 KBdavidwbarratt
#123 inner-diff.txt1.02 KBdavidwbarratt
#123 rest_cacheable_response-2765959-123.patch41.86 KBdavidwbarratt
#120 inner-diff.txt702 bytesdavidwbarratt
#120 rest_cacheable_response-2765959-120.patch41.76 KBdavidwbarratt
#117 inner-diff.txt626 bytesdavidwbarratt
#117 rest_cacheable_response-2765959-117.patch41.75 KBdavidwbarratt
#116 inner-diff.txt22.59 KBdavidwbarratt
#116 rest_cacheable_response-2765959-116.patch41.76 KBdavidwbarratt
#110 inner-diff.txt52.2 KBdavidwbarratt
#110 rest_cacheable_response-2765959-109.patch57.69 KBdavidwbarratt
#106 inner-diff.txt812 bytesdavidwbarratt
#106 rest_cacheable_response-2765959-106.patch25.05 KBdavidwbarratt
#104 inner-diff.txt7.24 KBdavidwbarratt
#104 rest_cacheable_response-2765959-104.patch25.12 KBdavidwbarratt
#99 inner-diff.txt2.33 KBdavidwbarratt
#99 rest_cacheable_response-2765959-99.patch23.3 KBdavidwbarratt
#97 inner-diff.txt7.12 KBdavidwbarratt
#97 rest_cacheable_response-2765959-97.patch20.96 KBdavidwbarratt
#94 inner-diff.txt1.14 KBdavidwbarratt
#94 rest_cacheable_response-2765959-94.patch21.62 KBdavidwbarratt
#92 inner-diff.txt6.04 KBdavidwbarratt
#92 rest_cacheable_response-2765959-91.patch21.32 KBdavidwbarratt
#91 rest_cacheable_response-2765959-90.patch19.55 KBdavidwbarratt
#76 inner-diff.txt9.21 KBdavidwbarratt
#76 D8.1-rest_cacheable_response-2765959-69-76.patch22.61 KBdavidwbarratt
#72 inner-diff.txt1.32 KBdavidwbarratt
#72 D8.1-rest_cacheable_response-2765959-69-72.patch15.27 KBdavidwbarratt
#67 rest_cacheable_response-2765959-67.patch18.96 KBdavidwbarratt
#67 inner-diff.txt2.33 KBdavidwbarratt
#62 rest_cacheable_response-2765959-62.patch15.7 KBdavidwbarratt
#62 inner-diff.txt1.23 KBdavidwbarratt
#60 inner-diff.txt1.19 KBdavidwbarratt
#60 rest_cacheable_response-2765959-60.patch14.78 KBdavidwbarratt
#54 rest_cacheable_response-2765959-54.patch14.24 KBdavidwbarratt
#55 D8.2-rest_cacheable_response-2765959-54.patch11.17 KBdavidwbarratt
#34 rest_cacheable_response-2765959-34.patch11.45 KBdavidwbarratt
#34 inner-diff.txt472 bytesdavidwbarratt
#30 rest_cacheable_response-2765959-30.patch9.71 KBdavidwbarratt
#30 inner-diff.txt706 bytesdavidwbarratt
#29 inner-diff.txt4.88 KBdavidwbarratt
#29 rest_cacheable_response-2765959-29.patch9.71 KBdavidwbarratt
#27 inner-diff.txt1.35 KBdavidwbarratt
#27 rest_cacheable_response-2765959-27.patch6.19 KBdavidwbarratt
#20 rest_cacheable_response-2765959-20.patch6.13 KBdavidwbarratt
#20 inner-diff.txt1.77 KBdavidwbarratt
#5 rest_cacheable_response-2765959-5.patch2.25 KBdavidwbarratt
#7 D8.0-rest_cacheable_response-2765959-6.patch3.03 KBdavidwbarratt
#5 inner-diff.txt0 bytesdavidwbarratt
#4 D8.0-rest_cacheable_response-2765959-2-do-not-test.patch2.23 KBdavidwbarratt
#2 rest_cacheable_response-2765959-2.patch2.25 KBdavidwbarratt
#6 inner-diff.txt821 bytesdavidwbarratt
#6 rest_cacheable_response-2765959-6.patch3.05 KBdavidwbarratt
#19 inner-diff.txt7.42 KBdavidwbarratt
#19 rest_cacheable_response-2765959-19.patch4.36 KBdavidwbarratt
#24 inner-diff.txt2.31 KBdavidwbarratt
#24 rest_cacheable_response-2765959-24.patch6.19 KBdavidwbarratt
#31 D8.0-rest_cacheable_response-2765959-30.patch8.59 KBdavidwbarratt
#33 inner-diff.txt1.49 KBdavidwbarratt
#33 rest_cacheable_response-2765959-33.patch11.2 KBdavidwbarratt
#35 D8.0-rest_cacheable_response-2765959-34.patch10.29 KBdavidwbarratt
#48 inner-diff.txt2.67 KBdavidwbarratt
#48 rest_cacheable_response-2765959-48.patch12.16 KBdavidwbarratt
#56 inner-diff.txt435 bytesdavidwbarratt
#56 rest_cacheable_response-2765959-56.patch14.23 KBdavidwbarratt
#64 inner-diff.txt2.82 KBdavidwbarratt
#64 rest_cacheable_response-2765959-64.patch16.63 KBdavidwbarratt
#71 D8.1-rest_cacheable_response-2765959-69.patch14.14 KBdavidwbarratt
#70 D8.2-rest_cacheable_response-2765959-69.patch16.28 KBdavidwbarratt
#69 inner-diff.txt3.3 KBdavidwbarratt
#69 rest_cacheable_response-2765959-69.patch19.85 KBdavidwbarratt
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

davidwbarratt created an issue. See original summary.

davidwbarratt’s picture

Version: 8.1.x-dev » 8.2.x-dev
Status: Active » Needs review
FileSize
2.25 KB

Status: Needs review » Needs work

The last submitted patch, 2: rest_cacheable_response-2765959-2.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
2.23 KB

Here's a Drupal 8.0.x version of the patch.

davidwbarratt’s picture

davidwbarratt’s picture

davidwbarratt’s picture

davidwbarratt’s picture

davidwbarratt’s picture

Issue summary: View changes

The last submitted patch, 5: rest_cacheable_response-2765959-5.patch, failed testing.

davidwbarratt’s picture

Issue summary: View changes
davidwbarratt’s picture

Issue summary: View changes
Wim Leers’s picture

Issue tags: +Needs tests

Thanks so much! Especially for your crystal-clear bug report, but also the patch! :) Much appreciated!

+++ b/core/modules/rest/src/RequestHandler.php
@@ -128,7 +129,14 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
-      return new Response($content, $e->getStatusCode(), $headers);
+      if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) {
+        $response = new CacheableResponse($content, $e->getStatusCode(), $headers);
+        $response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
+        return $response;
+      }
+      else {
+        return new Response($content, $e->getStatusCode(), $headers);
+      }

This is the wrong place to be making this change. The problem is that this is lacking cacheability metadata for the resource itself.

AFAICT this is repurposing the 4xx-response cache tag for a different purpose, that just happens to work in the special case you have, but it won't work in every case.

The root cause here is that the REST module was architected in a time where the cache system in D8 wasn't matured yet. So it uses the Symfony convention of

if (!$entity_access->isAllowed()) {
      throw new AccessDeniedHttpException();
}

… which then also throws away cacheability metadata in the access check result.

The proper solution would be to not throw that exception in EntityResource::get() anymore, but instead send a CacheableResponse() with a 403 status, and the cacheability metadata stored in $entity_access:
return (new CacheableResponse('', 403))->addCacheableDependency($entity_access);

davidwbarratt’s picture

I thought about doing that, as a 403 should really have the node:123 tag or something like that, but I wasn't sure if there was a way to do that in the rest module or if every resource was going to have to be responsible for adding it. :/

Wim Leers’s picture

Well, yes, every REST resource whose access depends on modifiable data (like an entity) will have to return a 403/404 response with the necessary cacheability metadata, they should not use the Symfony exceptions. That is annoying (but is only because Symfony's HTTP kernel/response objects/etc design is frankly quite short-sighted), but necessary. It won't be necessary for many REST resources though: most don't have this advanced access checking logic. And for those that do, this would mean core would be providing the right example.

dawehner’s picture

Well, it feels like optimizing for 400 errors is kind of a weird one. If people want to, they can always bypass caching easily for those.

davidwbarratt’s picture

It would be nice if we had exceptions that allowed you to add cachability metadata to them, but I guess what you're saying is the best solution for now.

I'll take a stab at it.

davidwbarratt’s picture

Status: Needs review » Active

What should we do for 404's? Right now, we just get this uncached response:

{
  "message": "The \"node\" parameter was not converted for the path \"/node/{node}\" (route name: \"rest.entity.node.GET.json\")"
}
davidwbarratt’s picture

Status: Active » Needs review
FileSize
4.36 KB
7.42 KB
davidwbarratt’s picture

Add the clearing of the 4xx-response tags back.

Status: Needs review » Needs work

The last submitted patch, 19: rest_cacheable_response-2765959-19.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review

The last submitted patch, 20: rest_cacheable_response-2765959-20.patch, failed testing.

davidwbarratt’s picture

Always return the access result as an object.

Wim Leers’s picture

Status: Needs review » Needs work

This patch looks great to me. It's only missing explicit tests for entity REST resources first resulting in a 404 or 403 and then after modifying the entity resulting in a 200.

The last submitted patch, 24: rest_cacheable_response-2765959-24.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
6.19 KB
1.35 KB

Resolving existing tests...

Status: Needs review » Needs work

The last submitted patch, 27: rest_cacheable_response-2765959-27.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
9.71 KB
4.88 KB
davidwbarratt’s picture

davidwbarratt’s picture

Drpual 8.0.x version

The last submitted patch, 29: rest_cacheable_response-2765959-29.patch, failed testing.

davidwbarratt’s picture

I realized there are times where there is no cachable metadata to return, so to make an easier DX; let's just change Response to CacheableResponse. This will make responses cacheable by default (which seems to be the norm). It also makes existing REST resources backwards-compatible with the change. Lastly, it allows people to customize the cache policies, where before it was always not cachable.

davidwbarratt’s picture

davidwbarratt’s picture

Drupal 8.0.x version (for anyone still using that version).

The last submitted patch, 33: rest_cacheable_response-2765959-33.patch, failed testing.

Wim Leers’s picture

Status: Needs review » Needs work
Issue tags: -Needs tests
  1. +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
    @@ -34,7 +34,7 @@ protected static function getPriority() {
    -    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN);
    +    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN);
    
    @@ -45,7 +45,7 @@ public function on403(GetResponseForExceptionEvent $event) {
    -    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND);
    +    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND);
    
    @@ -56,7 +56,7 @@ public function on404(GetResponseForExceptionEvent $event) {
    -    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED);
    +    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED);
    
    @@ -67,7 +67,7 @@ public function on405(GetResponseForExceptionEvent $event) {
    -    $response = new JsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE);
    +    $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE);
    

    Why are we changing these?

    There's no cacheability metadata for exceptions.

  2. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -95,7 +96,9 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    -          return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
    +          $response = new CacheableResponse($content, 400, ['Content-Type' => $request->getMimeType($format)]);
    +          $response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
    
    @@ -125,7 +128,9 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    -      return new Response($content, $e->getStatusCode(), $headers);
    +      $response = new CacheableResponse($content, $e->getStatusCode(), $headers);
    +      $response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
    

    These seem out of scope for the same reason as the first point.

  3. +++ b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
    @@ -127,7 +128,7 @@ protected function setEventResponse(GetResponseForExceptionEvent $event, $status
    -    $response = new Response($encoded_content, $status);
    +    $response = new CacheableResponse($encoded_content, $status);
    

    This one also seems out of scope for the same reason as the first point.

i.e. the scope of this issue should be to explicitly support 403/404 responses for EntityResource responses having the appropriate cache tags. It should not change "responses for exceptions", because those don't carry cacheability metadata anyway.

davidwbarratt’s picture

See #33.

Basically, if you have a REST Resource where it's a 404, there is no cachable metadata because there is no resource. To make an easier DX, let's just allow the 404 to get cached (if the policies allow it). If not, it will be uncached as usual.

But I don't see any reason a dev needs do this:

return new ResourceResponse(NULL, 404);

when they could just do this:

throw new NotFoundException();

which really should do the same thing.

Wim Leers’s picture

They DO NOT need to do

return new ResourceResponse(NULL, 404);

They need to do

$response = new ResourceResponse(NULL, 404);
$response->addCacheableDependency($acces_result_object);
return $response;

Because you need a response object that implements \Drupal\Core\Cache\CacheableResponseInterface to be able to add cacheability metadata.


throw new NotFoundException();

can never do the same thing, because an exception does not carry cacheability metadata. It could, but it doesn't. And doing that would require Drupal developers to never use Symfony's HttpException classes, but only Drupal's "cacheability metadata-compatible exceptions". Which is not realistic.


So, let's please focus only on the concrete problem, and not try to solve the generic problem. Because solving the generic problem would require 10 times as many changes. Feel free to create a new issue for that though, if you feel passionate about this.

davidwbarratt’s picture

Where does

$acces_result_object

come from on a 404? There is no entity, what are we checking access against?

davidwbarratt’s picture

From core, take a look at Drupal\dblog\Plugin\rest\resource\DBLogResource

  public function get($id = NULL) {
    if ($id) {
      $record = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
        ->fetchAssoc();
      if (!empty($record)) {
        return new ResourceResponse($record);
      }

      throw new NotFoundHttpException(t('Log entry with ID @id was not found', array('@id' => $id)));
    }

    throw new BadRequestHttpException(t('No log entry ID was provided'));
  }

Is there any cachable metadata in there?

dawehner’s picture

I still would like to question this issue with my comment #16

davidwbarratt’s picture

We are just making REST resources behave just like HTML resources. In the HTML resources, 4xx errors are cachable (per the cachability policies in core). If someone wants to cache them (or not cache them) with this change, they can do that.

Without this change you are forced to manually modify the Cache-Control header and manually collect the cache tags. :(

davidwbarratt’s picture

Also, if both of you could take a look at Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber in core, you'll see that for Exceptions that return HTML, a cachable response is always returned. I think the same should happen for any format (HTML, JSON, XML, etc.). The patch in #34 makes it consistent regardless of the format.

Wim Leers’s picture

Where does $acces_result_object come from on a 404? There is no entity, what are we checking access against?

True, you can't have that in case of a 404. So I guess I'm saying I so far am only convinced by the 403 handling. Although even in that case you have authentication to deal with, so … I wonder to what extent it's even cacheable.

Is there any cachable metadata in there?

No, there isn't. And that's why that response cannot be cacheable safely(in the sense that it can never be guaranteed to be up to date if it were made cacheable, because there are no cache tags for DB log entries). Entities do have cache tags, so there we can do that.


I still would like to question this issue with my comment #16

Hm good point. @davidwbarratt said: We are just making REST resources behave just like HTML resources. — except one of the big reasons we make 4xx HTML responses cacheable by PageCache is because search engine spiders/robots tend to cause a lot of requests. Having many requests to REST routes is less likely. Even less so because usually you need to be authenticated. And if you're authenticated, the Page Cache is no use anyway.

@davidwbarratt: I'm guessing that in your use case, you're doing REST requests for anonymous users? (i.e. anonymous users can access those routes?)

@dawehner: I think that in general it'd be interesting to have explicit support for REST+PageCache for anonymous user use cases, and for REST+Dynamic Page Cache for authenticated user use cases.

davidwbarratt’s picture

Without this change, if you do a 404 on a node route, you get this:

$ curl -I https://example.com/node/6546546546516541?_format=json
HTTP/1.1 404 Not Found
Date: Thu, 28 Jul 2016 00:27:06 GMT
Server: Apache/2.4.16 (Unix) PHP/5.6.20 OpenSSL/0.9.8zg
X-Content-Type-Options: nosniff
X-Powered-By: PHP/5.6.20
Cache-Control: must-revalidate, no-cache, post-check=0, pre-check=0, private
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
X-Generator: Drupal 8 (https://www.drupal.org)
Content-Type: application/json

With the patch you get this:

$ curl -I https://example.com/node/6546546546516541?_format=json
HTTP/1.1 404 Not Found
Date: Thu, 28 Jul 2016 00:28:48 GMT
Server: Apache/2.4.16 (Unix) PHP/5.6.20 OpenSSL/0.9.8zg
X-Content-Type-Options: nosniff
X-Powered-By: PHP/5.6.20
Cache-Control: max-age=21600, public
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Drupal-Cache-Tags: 4xx-response
X-Drupal-Cache-Contexts: 
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Last-Modified: Thu, 28 Jul 2016 00:28:48 GMT
ETag: "1469665728"
Vary: Cookie
X-Generator: Drupal 8 (https://www.drupal.org)
Purge-Cache-Tags: 4xx-response
Content-Type: application/json

Even without any entity at all, you do get cache tags, just like you would if you get an HTML version of the the page:

$ curl -I https://example.com/node/6546546546516541
HTTP/1.1 404 Not Found
Date: Thu, 28 Jul 2016 00:29:57 GMT
Server: Apache/2.4.16 (Unix) PHP/5.6.20 OpenSSL/0.9.8zg
X-Content-Type-Options: nosniff
X-Powered-By: PHP/5.6.20
Cache-Control: must-revalidate, no-cache, post-check=0, pre-check=0, private
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Drupal-Cache-Tags: 4xx-response block_view config:block.block.breadcrumbs config:block.block.help config:block.block.mainpagecontent config:block.block.seven_local_actions config:block.block.seven_messages config:block.block.seven_page_title config:block.block.seven_primary_local_tasks config:block.block.seven_secondary_local_tasks config:block_list config:user.role.anonymous rendered
X-Drupal-Cache-Contexts: languages:language_interface route theme url user.permissions
Expires: Sun, 19 Nov 1978 05:00:00 GMT
X-Generator: Drupal 8 (https://www.drupal.org)
Purge-Cache-Tags: 4xx-response block_view config:block.block.breadcrumbs config:block.block.help config:block.block.mainpagecontent config:block.block.seven_local_actions config:block.block.seven_messages config:block.block.seven_page_title config:block.block.seven_primary_local_tasks config:block.block.seven_secondary_local_tasks config:block_list config:user.role.anonymous rendered
Content-Type: text/html; charset=UTF-8

I'm not actually sure what's setting the html response to not be cached (it might just be something in my configuration). But regardless, in the first example, there isn't a way to cache it at all, in the second and third, you can modify the cache handling.

Since the ParamConverter throws the NotFoundException, there isn't a place to change this to ResourceResponse (and I think it would be inappropriate to do so). However, it is imperative that there be a way to cache the response.

Yes, our REST endpoints are publically accessible, and there is a high possibility that we could have a large number of 404 hits. For instance, if someone referenced an article from a page, and then that article was deleted, the reference to the existing article would still be there. Because of this, when an application tries to access the article, they would get a 404 response. Even if the initial page is cached, the request for the related article would not be.

As an example, the page could be hit 1,000,000 times. All of these would be cache hits except for the first request. However, because of the 404 reference, we would go from 1,000,000 cache hits on the article resource, to 0 cache hits on the article resource and 1,000,000 hits to the origin server. What wasn't an issue at all, becomes a major issue that can bring down an origin server just because an editor deletes an article and are unaware of the reference.

This has actually happened to us, in production, on Acquia (if you want to look it up). :)

dawehner’s picture

Title: Drupal\rest\RequestHandler should return a CacheableResponse for client errors » Drupal\rest\RequestHandler should return a CacheableResponse for client errors on GET requests

Thank you for explaining a bit what is going on. Just to be clear, if someone is attacking your site though, you have a hard time in Drupal anyway as its trivial to bypass caching layers.

IMHO we should rename the issue title and focus the issue a bit more on GET requests, see some reasons below.

  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -82,8 +85,11 @@ public function post(EntityInterface $entity = NULL) {
    +    $entity_access = $entity->access('create', NULL, TRUE);
    +    if (!$entity_access->isAllowed()) {
    +      $response = new ResourceResponse(NULL, 403);
    +      $response->addCacheableDependency($entity_access);
    +      return $response;
    
    @@ -101,8 +107,14 @@ public function post(EntityInterface $entity = NULL) {
    +      $field_access = $entity->get($field_name)->access('edit', NULL, TRUE);
    +      if (!$field_access->isAllowed()) {
    +        $response = new ResourceResponse([
    +          'error' => "Access denied on creating field '$field_name'"
    +        ], 403);
    +        $response->addCacheableDependency($entity_access);
    +        $response->addCacheableDependency($field_access);
    +        return $response;
    
    @@ -145,8 +157,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +    $original_entity_access = $original_entity->access('update', NULL, TRUE);
    +    if (!$original_entity_access->isAllowed()) {
    +      $response = new ResourceResponse(NULL, 403);
    +      $response->addCacheableDependency($original_entity);
    +      $response->addCacheableDependency($original_entity_access);
    +      return $response;
         }
    
    @@ -159,8 +175,15 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +      $field_access = $original_entity->get($field_name)->access('edit', NULL, TRUE);
    +      if (!$field_access->isAllowed()) {
    +        $response = new ResourceResponse([
    +          'error' => "Access denied on updating field '$field_name'."
    +        ], 403);
    +        $response->addCacheableDependency($original_entity);
    +        $response->addCacheableDependency($original_entity_access);
    +        $response->addCacheableDependency($field_access);
    +        return $response;
    
    @@ -191,8 +214,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +    $entity_access = $entity->access('delete', NULL, TRUE);
    +    if (!$entity_access->isAllowed()) {
    +      $response = new ResourceResponse(NULL, 403);
    +      $response->addCacheableDependency($entity);
    +      $response->addCacheableDependency($entity_access);
    +      return $response;
    

    Its honestly a bit weird that we add cacheabiliy metadata for non GET/HEAD requests ... These access denied conditions vary by the submitted fields for example, so do we vary somewhere by the HTTP body? I don't see that. Yes, DELETE requests are theoretically cacheable, given they are idempotent, but that's a weird optimization strategy

  2. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -7,6 +7,7 @@
    @@ -66,7 +67,9 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    
    @@ -66,7 +67,9 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
             catch (UnexpectedValueException $e) {
               $error['error'] = $e->getMessage();
               $content = $serializer->serialize($error, $format);
    -          return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
    +          $response = new CacheableResponse($content, 400, ['Content-Type' => $request->getMimeType($format)]);
    +          $response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
    

    Just reading that code ... we are throwing an exception and then assuming we know how to cache that. Do we know what varied in order to throw the exception? To do this right one would need for example some kind of cache context for the serialized value at least. I don't see that.

davidwbarratt’s picture

Title: Drupal\rest\RequestHandler should return a CacheableResponse for client errors on GET requests » Drupal\rest\RequestHandler should return a CacheableResponse for client errors
Status: Needs work » Needs review
FileSize
12.16 KB
2.67 KB
  1. If you take a look at CommandLineOrUnsafeMethod::check we're explicitly denying cachability to any "unsafe" methods. The unsafe methods can be found in Request::isMethodSafe since only GET and HEAD are safe methods those are the only ones that will be cached. Unless a developer wants to explicitly override the cache policy (which they should be allowed to do).
  2. You're right. Since there is no cachable metadata available, we shouldn't be making any assumptions on the metadata at all. I've attached a patch that will throw an exception in that instance and in the other instance the exception will simply bubble up. This means that the REST module is no longer making any assumptions, and if you throw an exception it's handled (or not handled) by the existing cache policies.

The last submitted patch, 48: rest_cacheable_response-2765959-48.patch, failed testing.

dawehner’s picture

If you take a look at CommandLineOrUnsafeMethod::check we're explicitly denying cachability to any "unsafe" methods. The unsafe methods can be found in Request::isMethodSafe since only GET and HEAD are safe methods those are the only ones that will be cached. Unless a developer wants to explicitly override the cache policy (which they should be allowed to do).

I would totally agree in the case we would get the cacheability metadata right, but for example for POST requests one would have to vary by the POST data, but we don't yet.
Given that IMHO we should either implement it correctly, or not at all.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

davidwbarratt’s picture

That's my point though, we already do not cache POST requests. This patch does not change that at. It does enable a developer to cache them if they want to do that properly. Without this patch there is no way to do that (without overriding the plugin I guess).

davidwbarratt’s picture

So #34 has a bug where if a response throws a NotAcceptableHttpException then it returns these headers:

$ curl -I https://8.golfchannel.loc/block-layout?_format=json&path=%2Fblock-layout
HTTP/1.1 406 Not Acceptable
Date: Mon, 15 Aug 2016 17:25:57 GMT
Server: Apache/2.4.16 (Unix) PHP/5.6.21 OpenSSL/0.9.8zg
X-Content-Type-Options: nosniff
X-Powered-By: PHP/5.6.21
Cache-Control: max-age=21600, public
X-Drupal-Dynamic-Cache: HIT
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Drupal-Cache-Tags: 4xx-response config:rest.settings config:user.role.anonymous
X-Drupal-Cache-Contexts: user.permissions
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Last-Modified: Mon, 15 Aug 2016 17:25:57 GMT
ETag: "1471281957"
Vary: Cookie
X-Generator: Drupal 8 (https://www.drupal.org)
Purge-Cache-Tags: 4xx-response config:rest.settings config:user.role.anonymous
Content-Type: application/json

Then if you go back to a response that should return a 200, you get the same cached response back as the error.

I believe this is because in that patch the Cache-Context is assumed to be user.permissions.

It looks like what I have in #48 is correct. Since we are not making any assumptions on the Cache-Contexts, then we shouldn't have any Cache-Contexts in the response. Without any cache contexts, Drupal marks the request as uncachable.

davidwbarratt’s picture

davidwbarratt’s picture

davidwbarratt’s picture

Status: Needs review » Needs work

The last submitted patch, 54: rest_cacheable_response-2765959-54.patch, failed testing.

The last submitted patch, 55: D8.2-rest_cacheable_response-2765959-54.patch, failed testing.

The last submitted patch, 56: rest_cacheable_response-2765959-56.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 60: rest_cacheable_response-2765959-60.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
15.7 KB
1.23 KB

The last submitted patch, 62: rest_cacheable_response-2765959-62.patch, failed testing.

davidwbarratt’s picture

Status: Needs review » Needs work
FileSize
16.63 KB
2.82 KB
davidwbarratt’s picture

Status: Needs work » Needs review

The last submitted patch, 64: rest_cacheable_response-2765959-64.patch, failed testing.

davidwbarratt’s picture

The last submitted patch, 67: rest_cacheable_response-2765959-67.patch, failed testing.

davidwbarratt’s picture

davidwbarratt’s picture

davidwbarratt’s picture

davidwbarratt’s picture

davidwbarratt’s picture

The last submitted patch, 71: D8.1-rest_cacheable_response-2765959-69.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 72: D8.1-rest_cacheable_response-2765959-69-72.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
22.61 KB
9.21 KB
dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
    @@ -34,7 +34,7 @@ protected static function getPriority() {
        */
       public function on403(GetResponseForExceptionEvent $event) {
    -    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN);
    +    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN);
         $event->setResponse($response);
       }
     
    @@ -45,7 +45,7 @@ public function on403(GetResponseForExceptionEvent $event) {
    
    @@ -45,7 +45,7 @@ public function on403(GetResponseForExceptionEvent $event) {
        *   The event to process.
        */
       public function on404(GetResponseForExceptionEvent $event) {
    -    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND);
    +    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND);
         $event->setResponse($response);
       }
     
    @@ -56,7 +56,7 @@ public function on404(GetResponseForExceptionEvent $event) {
    
    @@ -56,7 +56,7 @@ public function on404(GetResponseForExceptionEvent $event) {
        *   The event to process.
        */
       public function on405(GetResponseForExceptionEvent $event) {
    -    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED);
    +    $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED);
         $event->setResponse($response);
       }
     
    @@ -67,7 +67,7 @@ public function on405(GetResponseForExceptionEvent $event) {
    
    @@ -67,7 +67,7 @@ public function on405(GetResponseForExceptionEvent $event) {
        *   The event to process.
        */
       public function on406(GetResponseForExceptionEvent $event) {
    -    $response = new JsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE);
    +    $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE);
         $event->setResponse($response);
       }
    

    Mh, so by what does this cacheable response varies at that point? All those responses vary by nothing, which, can't be right.

  2. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -96,11 +105,19 @@ public function post(EntityInterface $entity = NULL) {
    +      $field_access = $entity->get($field_name)->access('edit', NULL, TRUE);
    +      if (!$field_access->isAllowed()) {
    +        $response = new ResourceResponse([
    +          'message' => "Access denied on creating field '$field_name'"
    +        ], 403);
    +        $response->addCacheableDependency($entity_access);
    +        $response->addCacheableDependency($field_access);
    +        return $response;
           }
    

    Don't we ideally have to check access of all fields and then combine that? In general this also feels like an API break to move away from the exceptions into directly returning the response.

dawehner’s picture

Status: Needs review » Needs work
davidwbarratt’s picture

Status: Needs work » Needs review
  1. I mean I think the problem is, is that if you don't provide cachable metadata, how could we possible know the cache contexts? I believe if there are no Contexts, Drupal's Dynamic Cache will not cache it at all (which I think is fine because there is nothing to cache it by). I suppose they would be different cache contexts depending on the error (i.e. a 404/403 would be url) where a 405 would be url headers:status)? I think the real issue is, is that you should not be using the HTTP Exceptions unless there is no cachable metadata (see #39)
  2. I thought about that, but the error message is specific to that specific field. We also return a response as soon as we encounter a field that we don't have access to. If we want to add all fields, we'd first have to check all access, then loop through them again to determine if any of them should end the request. I'm fine with it either way, I wasn't sure which one was the more proper way to do it. If you want I can change that.
davidwbarratt’s picture

Perhaps we should go through the Cache Contexts that are used on html requests and apply the ones that would apply on non-html request (i.e. I don't think theme would apply).

dawehner’s picture

I thought about that, but the error message is specific to that specific field. We also return a response as soon as we encounter a field that we don't have access to. If we want to add all fields, we'd first have to check all access, then loop through them again to determine if any of them should end the request. I'm fine with it either way, I wasn't sure which one was the more proper way to do it. If you want I can change that.

Well, this wasn't about the specific message. The problem is that when you want to cache something you need to know all factors by which this request could vary. Given that, we need to know all access result objects for every field access. You can beside of that still show just the error message of the first failed field.

I mean I think the problem is, is that if you don't provide cachable metadata, how could we possible know the cache contexts? I believe if there are no Contexts, Drupal's Dynamic Cache will not cache it at all (which I think is fine because there is nothing to cache it by). I suppose they would be different cache contexts depending on the error (i.e. a 404/403 would be url) where a 405 would be url headers:status)? I think the real issue is, is that you should not be using the HTTP Exceptions unless there is no cachable metadata (see #39)

Well, you know, throwing those exceptions are part of our API, and they have been longer than any render caching. If code cannot longer rely on being executed, I am quite convinced we add way more bugs, than we try to solve here.

davidwbarratt’s picture

If you take a look at DBLogResource::get() what should the cache contexts for the response (or the exceptions) be? I suppose it could/should always be varied by url.path?

davidwbarratt’s picture

I think it's ok to throw the exceptions when you don't have any cachable metadata, but that shouldn't prevent the request from being cached (it doesn't in html format, so why would it in any other format?)

dawehner’s picture

Yeah, this would vary by url.path, as well as this would require something like a cache tag for watchdog entries. As you see, this all potential causes some BC issues.

davidwbarratt’s picture

Watchdog entries don't have cache tags because they wouldn't be cleared out when you create, update, or delete an entry. So even if we added one, it wouldn't help since it's not an entity.

I could take a stab at adding contexts to the exceptions, I mean they will be pretty broad. But right now, like I've said, you can throw an exception in an HTML response and you get a cachable response (with default cache contexts and cache tags).

As far as the BC break. I think this is a bug, it's not a feature request. We are standardizing the API to have a consistent response between the html format and other formats. I think it was a complete oversight that these responses were not cachable in the first place (see #13).

dawehner’s picture

BC breaks should still not be introduced as part of fixing bugs, as we basically replace one bug fix by another :)

davidwbarratt’s picture

Fine... so is there a way to cache 404's without a BC break? I should say there are two different instances of a 404, there's a route not found exception as well as a param not converted exception.

Since everything is returned as a standard Response object, I don't think there's a way to do it, other than to introduce another middleware I guess?

davidwbarratt’s picture

I guess what we could do, is create an Experimental module that would override the exception handler?

dawehner’s picture

I think making the changes just to the ResourceHandler would be way more safe, than changing the generic exception subscriber, which might be used by a lot of other code. Does that make sense?

davidwbarratt’s picture

Well, that's what I started doing in #6, but by #34 I figured out that that wasn't going to work. I explained in #46.

The problem is, is that if you pass a non-existant route, or a route with a bad paramter, the routing system throws an exception before it even gets to the REST module.

For instance, if you try to hit http://www.example.com/node/16354351?_format=json then the ParamConverterManager::convert() throws a ParamNotConvertedException.

If this is in html format, it's a cachable response, if it's in any other format, the response is not cachable. I think it would be best if it was cachable regardless of the format.

Let me know if you can think of a way to do this in the best BC possible way, #69 is the best I could come up with.

Alternatively, the only way I can think to do this is by making a module that overrides the exception handler as well as the REST RequstHandler. I think that's monkey-patching, but it would work and wouldn't break BC.

davidwbarratt’s picture

davidwbarratt’s picture

Adding support for 401 which is for some reason missing. :(

The last submitted patch, 91: rest_cacheable_response-2765959-90.patch, failed testing.

davidwbarratt’s picture

Resolve the error(s).

The last submitted patch, 92: rest_cacheable_response-2765959-91.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 94: rest_cacheable_response-2765959-94.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
20.96 KB
7.12 KB

Remove code leakage and resolve tests.

Status: Needs review » Needs work

The last submitted patch, 97: rest_cacheable_response-2765959-97.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
23.3 KB
2.33 KB

I saw this and I thought it was code leakage from somewhere, but it looks like it resolves one of the tests... it sure looks funky though. Not sure how else to resolve it.

davidwbarratt’s picture

Oh I see... because we are no longer throwing an exception, we have to catch the 403 response, rather than the 403 exception.

Status: Needs review » Needs work

The last submitted patch, 99: rest_cacheable_response-2765959-99.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 99: rest_cacheable_response-2765959-99.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
25.12 KB
7.24 KB

Moving the cache context to a response subscriber and ensuring that cachable metadata is not overridden when returning a response rather than an exception.

Status: Needs review » Needs work

The last submitted patch, 104: rest_cacheable_response-2765959-104.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
25.05 KB
812 bytes

Asserting that there is no response. Since we are using the existing response object rather than throwing an exception, the message cannot be passed along and serialized. :(

The choice is either to have cachable metadata passed along, or to have the message passed. I'm going to opt for the cachable metadata.

This is making me realize that more and more we need a CachableHttpException. It would be nice to be able to throw an exception and be able to add cacheable metadata to that exception like you can with a CacheableResponse. That would fix this problem because you could have both the message as well as the metadata.

However, that is a lot of changes and while I suppose it could be BC because we could support cachable and non-cachable exceptions (like we do for responses) it just feels like a lot of architectural changes. Then again, if that is overall a better solution, then that's probably the direction we should go.

Wim Leers’s picture

Status: Needs review » Needs work

Nice progress here!

  1. +++ b/core/lib/Drupal/Core/Entity/Entity.php
    @@ -527,10 +527,8 @@ protected function invalidateTagsOnSave($update) {
    -    if ($this->hasLinkTemplate('canonical')) {
    -      // Creating or updating an entity may change a cached 403 or 404 response.
    -      $tags = Cache::mergeTags($tags, ['4xx-response']);
    -    }
    +    // Creating or updating an entity may change a cached 403 or 404 response.
    +    $tags = Cache::mergeTags($tags, ['4xx-response']);
    

    Why change this? Because the 4xx response can also happen for routes other than the canonical one?

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    @@ -125,6 +126,29 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
    +  public function onForbiddenResponse(FilterResponseEvent $event) {
    

    Why is this necessary? Why is the existing \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge() insufficient?

    I have suspicions, but the documentation on this method doesn't explain it at all. Nor is there explicit test coverage being added.

  3. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    --- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
    +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
    

    These changes are effectively duplicating \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber.

    We should consolidate them, not add more such implementations.

    I'd be fine with moving \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber into core/lib/Drupal/Core/EventSubscriber and deprecating the original one, and this ExceptionJsonSubscriber.

  4. +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
    @@ -34,8 +37,19 @@ protected static function getPriority() {
    +    $cache->setCacheContexts(['headers:authorization']);
    

    Hm… this is going to cache a separate response for every possible value of the Authorization header. That seems problematic too.

    Also, this points to missing test coverage for cached 4xx responses to prove me wrong :)

  5. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -106,7 +105,10 @@ public static function create(ContainerInterface $container, array $configuratio
    +      $response = new ResourceResponse(NULL, 403);
    +      $response->addCacheableDependency($entity);
    +      $response->addCacheableDependency($entity_access);
    

    Ah! This at least partially explains the changes in AuthenticationSubscriber.

    Related: #2808233: REST 403 responses don't tell the user *why* access is not granted: requires deep Drupal understanding to figure out.

    Finally: why this approach? Why not subclass AccessDeniedHttpException so that the exception can also carry cacheability metadata?
    That'd be a far less invasive change.

  6. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -144,8 +146,11 @@ public function post(EntityInterface $entity = NULL) {
    -    if (!$entity->access('create')) {
    -      throw new AccessDeniedHttpException();
    +    $entity_access = $entity->access('create', NULL, TRUE);
    +    if (!$entity_access->isAllowed()) {
    
    @@ -199,8 +217,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    -    if (!$original_entity->access('update')) {
    -      throw new AccessDeniedHttpException();
    +    $original_entity_access = $original_entity->access('update', NULL, TRUE);
    +    if (!$original_entity_access->isAllowed()) {
    
    @@ -256,8 +285,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    -    if (!$entity->access('delete')) {
    -      throw new AccessDeniedHttpException();
    +    $entity_access = $entity->access('delete', NULL, TRUE);
    +    if (!$entity_access->isAllowed()) {
    

    Why these changes? PATCH/POST/DELETE responses are not cacheable by definition anyway.

  7. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -256,8 +285,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
    
    diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
    deleted file mode 100644
    

    Why delete this trait?

  8. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -88,6 +90,9 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    +      // Set the request format so an Exception is returned in the proper
    +      // format.
    +      $request->setRequestFormat($format);
    

    Interesting… this would definitely need explicit test coverage in \Drupal\Tests\rest\Kernel\RequestHandlerTest(). And it probably needs to happen in a separate issue.

  9. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -107,9 +112,7 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    -          $error['error'] = $e->getMessage();
    -          $content = $serializer->serialize($error, $format);
    -          return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
    +          throw new BadRequestHttpException($e->getMessage(), $e);
    

    This is already being fixed in #2813853: RequestHandler has its own error handling rather than leaving it to KernelEvents::EXCEPTION event subscribers.


EDIT: cross-posted with #106.

This is making me realize that more and more we need a CachableHttpException

+1. But please make it CacheableHttpExceptionInterface + CacheableHttpExceptionTrait. Then you can simply do:

class CacheableAccessDeniedHttpException extends AccessDeniedHttpException implements CacheableHttpExceptionInterface {
  use CacheableHttpExceptionTrait;
}
davidwbarratt’s picture

Thanks for the feedback!

  1. Yes. For instance, if you create a view that retrieves an entity at say /awesome/{id} that id will 404 which will be cached, creating the entity at that id should clear it out. That all works with entities that have a canonical link template, but if it doesn't, it doesn't purge them and there isn't an easy way to do it unless you add it to every entity that doesn't have the canonical link template.
  2. You probably already figured this out, but now that we are returning a response rather than an exception, we have to catch the response. If we change this to cachable exceptions we can remove this, which I think is the way to go.
  3. The problem is \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber requires the seralization service which is only available in the module. If we move it we'd also have to enable the module. I could make a more generic class and extend both of them from the same one, then just the constructors and ::setEventResponse would be different. Is that better?
  4. Sigh... the problem I was running into was that if you hit an endpoint without an Authorizaiton header you would get the 401 and be stuck because adding credentials would still return a 401 back to you. I haven't tested this with user.permission because I was accidentally overriding the cachable metadata. I'll see what happens with that context rather than the header context.
  5. Yeah seems like subclassing the exceptions is the way to go
  6. Because a developer can easily override the caching policy to allow caching for other methods. It's much more difficult to override this code to allow caching. I figured adding the cachable metadata at least made that a possability for people even though we don't support it.
  7. That was the only place the trait was used. I simplified the code and I didn't see a reason to leave it if it's only used in one class anyways.
  8. Yeah needs tests everywhere. :)
  9. Well I'll need to swap it out with the new exception classes anyways. :)

The last submitted patch, 106: rest_cacheable_response-2765959-106.patch, failed testing.

davidwbarratt’s picture

davidwbarratt’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 110: rest_cacheable_response-2765959-109.patch, failed testing.

Wim Leers’s picture

#108.3: yes that'd be better.

#108.6: no, caching for POST/PATCH/DELETE never makes sense. They must result in modifications to the stored data, or at least attempted modifications. Which means their responses can never be cached.

Retesting #110, because CI error.


  1. +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
    @@ -97,7 +98,7 @@ public function onKernelRequestFilterProvider(GetResponseEvent $event) {
    -        throw new AccessDeniedHttpException();
    +        throw new CacheableAccessDeniedHttpException();
           }
         }
       }
    @@ -119,6 +120,9 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
    
    @@ -119,6 +120,9 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
           if ($exception instanceof AccessDeniedHttpException && !$this->authenticationProvider->applies($request) && (!isset($this->filter) || $this->filter->appliesToRoutedRequest($request, FALSE))) {
             $challenge_exception = $this->challengeProvider->challengeException($request, $exception);
             if ($challenge_exception) {
    +          if ($exception instanceof CacheableHttpExceptionInterface && $challenge_exception instanceof CacheableHttpExceptionInterface) {
    +            $challenge_exception->addCacheableDependency($exception->getCacheableMetadata());
    +          }
               $event->setException($challenge_exception);
             }
           }
    @@ -126,32 +130,6 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
    
    @@ -126,32 +130,6 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
       }
     
       /**
    -   * Respond with a challenge on a 403 response if appropriate.
    -   *
    -   * On a 403 (access denied), if there are no credentials on the request, some
    -   * authentication methods (e.g. basic auth) require that a challenge is sent
    -   * to the client.
    -   *
    -   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
    -   *   The response event.
    -   */
    -  public function onForbiddenResponse(FilterResponseEvent $event) {
    -    if (isset($this->challengeProvider) && $event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
    -      $request = $event->getRequest();
    -      $response = $event->getResponse();
    -      if ($response->getStatusCode() === 403 && !$this->authenticationProvider->applies($request) && (!isset($this->filter) || $this->filter->appliesToRoutedRequest($request, FALSE))) {
    -        $challenge_exception = $this->challengeProvider->challengeException($request, new AccessDeniedHttpException());
    -        if ($challenge_exception) {
    -          // Add the exception data to the current response so cachable metadata
    -          // is not overridden.
    -          $response->headers->add($challenge_exception->getHeaders());
    -          $response->setStatusCode($challenge_exception->getStatusCode());
    -        }
    -      }
    -    }
    -  }
    

    So much better!

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
    @@ -158,6 +159,11 @@ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $st
    +      // Persist any cachable metadata.
    

    s/cachable/cacheability/

  3. +++ b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php
    @@ -77,7 +77,7 @@ protected function copyRawVariables(array $defaults) {
    -      $event->setException(new NotFoundHttpException($exception->getMessage(), $exception));
    +      $event->setException(new CacheableNotFoundHttpException($exception->getMessage(), $exception));
    
    +++ b/core/lib/Drupal/Core/Routing/RequestFormatRouteFilter.php
    @@ -45,7 +45,7 @@ public function filter(RouteCollection $collection, Request $request) {
    -    throw new NotAcceptableHttpException("No route found for the specified format $format.");
    +    throw new CacheableNotAcceptableHttpException("No route found for the specified format $format.");
    
    +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
    @@ -130,7 +130,7 @@ public function challengeException(Request $request, \Exception $previous) {
    -    return new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
    +    return new CacheableUnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
    

    Why modify these? They don't contain cacheability metadata.

    Perhaps you were trying to update all code throwing Symfony HTTP exceptions? I think that's not necessary; just updating the ones that do have cacheability metadata is sufficient.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
41.76 KB
22.59 KB

Based on #114, I've simplified the patch by removing any use of the new Cacheable Exceptions when the request is for certain not a GET request. When the request could be a GET request, a Cacheable Exception should be used, even if we are not adding any cacheable metadata. Making the exception cachable allows us to cache the request under certain conditions, if it's not a cacheable exception, then it will never be cached.

For this patch, I modified all of the places I could find that an HttpException could be thrown on a GET request in core. There might be more places, but I think I've covered most of them.

I think this is the cleanest solution so far. It resolves the problem and it doesn't introduce new issues (from what I can see). It also allows developers to add cacheable metadata to exceptions, which I think is an awesome new feature.

Probably should still add specific tests for this issue, not sure where they should go.

Also, is this a bc break? I don't think it is, but if it is, how should we resolve it? I guess we could add a config option? what would it do?

davidwbarratt’s picture

Resolving a Fatal PHP Error. :)

Status: Needs review » Needs work

The last submitted patch, 117: rest_cacheable_response-2765959-117.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
41.76 KB
702 bytes

Resolve another PHP Fatal Error.

davidwbarratt’s picture

Title: Drupal\rest\RequestHandler should return a CacheableResponse for client errors » Make HTTP Exceptions Cacheable

Status: Needs review » Needs work

The last submitted patch, 120: rest_cacheable_response-2765959-120.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
41.86 KB
1.02 KB

Status: Needs review » Needs work

The last submitted patch, 123: rest_cacheable_response-2765959-123.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
42.15 KB
3.31 KB

Responses need to be prepared from data in Request (how was this ever working??)

Status: Needs review » Needs work

The last submitted patch, 125: rest_cacheable_response-2765959-125.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
42.06 KB

To preserve backwards comparability (and resolve these tests). We will only send a CacheableResponse when a CacheableHttpException is thrown. If it is not, then a standard Response will be returned.

Status: Needs review » Needs work

The last submitted patch, 127: rest_cacheable_response-2765959-127.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
39.5 KB

Reroll

Status: Needs review » Needs work

The last submitted patch, 129: rest_cacheable_response-2765959-129.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
39.5 KB
685 bytes

Status: Needs review » Needs work

The last submitted patch, 131: rest_cacheable_response-2765959-131.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
37.72 KB

Status: Needs review » Needs work

The last submitted patch, 133: rest_cacheable_response-2765959-133.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
38.63 KB
932 bytes

Status: Needs review » Needs work

The last submitted patch, 135: rest_cacheable_response-2765959-135.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
39.16 KB
911 bytes

Status: Needs review » Needs work

The last submitted patch, 137: rest_cacheable_response-2765959-137.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
1.24 KB
37.92 KB

Status: Needs review » Needs work

The last submitted patch, 139: rest_cacheable_response-2765959-139.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
36.55 KB
1.37 KB
davidwbarratt’s picture

The last submitted patch, 141: rest_cacheable_response-2765959-141.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 142: rest_cacheable_response-2765959-142.patch, failed testing.

davidwbarratt’s picture

Status: Needs work » Needs review
FileSize
720 bytes
34.81 KB

Status: Needs review » Needs work

The last submitted patch, 145: rest_cacheable_response-2765959-145.patch, failed testing.

Wim Leers’s picture

#116: great work!

1) Drupal\Tests\hal\Functional\EntityResource\User\UserHalJsonAnonTest::testGet
Failed asserting that 403 is identical to 200.

/var/www/html/core/modules/rest/tests/src/Functional/ResourceTestBase.php:316
/var/www/html/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php:352

This is because the last line here is failing:

    // DX: 403 when unauthorized.
    $response = $this->request('GET', $url, $request_options);
    // @todo Update the message in https://www.drupal.org/node/2808233.
    $this->assertResourceErrorResponse(403, '', $response);


    $this->setUpAuthorization('GET');


    // 200 for well-formed HEAD request.
    $response = $this->request('HEAD', $url, $request_options);
    $this->assertResourceResponse(200, '', $response);

… which means that 403 it's asserting a few lines earlier is still being served. Despite that setUpAuthorization() line granting an additional permission to the tested role (anonymous in this case). This probably means that the cached response does not have the user.role:anonymous cache context, unless this patch is of course introducing some other bug.

Hopefully this helps getting the patch to a point where it passes all tests :)

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Wim Leers’s picture

I want to get this issue closed: either get it committed, or close it. This issue has been dragging on for too long already. So I went over this entire issue again. I read the IS, and every comment.

I do want to call out that since this issue started, and since the last patches that were posted, a lot has changed. Drupal 8 now has:
vastly expanded test coverage — for example: #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method
exception handling has been vastly improved (simpler, more robust, far more test coverage) — for example: #2739617: Make it easier to write on4xx() exception subscribers
vastly improved error responses — #2808233: REST 403 responses don't tell the user *why* access is not granted: requires deep Drupal understanding to figure out

All those changes do impact this issue.


#16: I agree that caching 4xx responses for REST is questionable, given that 95% of sites require authentication to access REST resources. However:

  1. Some sites provide anonymous REST access. For example drupal.org. @davidwbarratt apparently also has a need for this. We should support anonymous, cacheable 4xx REST GET responses (with cache tags).
  2. If we manage to fix #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster, then Dynamic Page Cache can in fact cache authenticated REST responses and vary them as needed. If we add support for that, then we should also support authenticated, cacheable 4xx REST GET responses (with cache tags and cache contexts).

(I wrote something similar at the very bottom of #45.)


#46: thanks for providing an actual real-world use case for supporting anonymous, cacheable 4xx REST GET responses (with cache tags). But, as #47 says, if this an actual attack, and not merely a massive amount of traffic, then it's easy to bypass this cache anyway: just perform requests to Drupal with a querystring that contains the current UNIX timestamp, or a random value, and you'll bypass all caches.


#47: fully agreed that this issue should only handle GET requests/responses.

#48 + #52: it doesn't matter that Page Cache will only cache GET and HEAD responses (and so will Varnish or any other HTTP cache btw). It never makes sense to cache responses to POST, PATCH or DELETE requests.

#53 + #79:

Since we are not making any assumptions on the Cache-Contexts, then we shouldn't have any Cache-Contexts in the response. Without any cache contexts, Drupal marks the request as uncachable.

I believe if there are no Contexts, Drupal's Dynamic Cache will not cache it at all (which I think is fine because there is nothing to cache it by)

This is wrong. If there are no cache contexts, then Drupal concludes there are no contexts by which to vary. In other words: there's only one variation of the response for this request. If this response ends up not being cached, that's then happening due to other reasons, it's not happening because there are no cache contexts.
If you don't believe me, read \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber::shouldCacheResponse():

    // Response has a high-cardinality cache context.
    if (array_intersect($cacheability->getCacheContexts(), $conditions['contexts'])) {
      return FALSE;
    }
…
    return TRUE;

There's no logic there for "no cache contexts are set, so then don't cache". Your response is not being cached due to other reasons.


#90: thanks for providing an overview of what you tried, and what did and did not work. It is very very helpful to be able to read your reasoning :)


I see that I've been fairly active in reviewing/responding from #107 onward. But then, as of #145, it looks like @davidwbarratt gave up. Understandable!

I think that the patch has been moving in the right direction in principle/in general. But I also think that we need #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster to be fixed before we can do this. Otherwise we still don't know if REST + Dynamic Page Cache + 4xx responses is actually working as intended. We do not need to wait for #2827797 to be fixed: that's merely improving how REST + Dynamic Page Cache work; it's making Dynamic Page Cache able to cache REST's responses more effectively.


Before diving deep into a patch review, let's see how much of this patch is passing tests. A lot has changed, many things which simplify this patch; many of the test failures I see in #145 are related to things that have been improved since then.

So, I rebased this patch on top of HEAD. Lots of details have changed. Several changes are no longer necessary. And it seems that many if not most of the test failures in #145 have disappeared :)

Status: Needs review » Needs work

The last submitted patch, 149: rest_cacheable_response-2765959-149.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
1.97 KB
31.99 KB

20 of the 41 test failures report this:

--- Expected
+++ Actual
@@ @@
-{"message":"The used authentication method is not allowed on this route."}
+{"message":"…"}

20 of the 41 test failures report this:

Failed asserting that 401 is identical to 403.

Both of them happening at line 363 in EntityResourceTestBase. Both have the same root cause.

This is because the response is actually a Page Cache HIT, instead of a Page Cache MISS. Why? Because the changes made in this patch cause the previous 4xx response to be cached and served!
In other words: this is causing incorrect 4xx responses, because the response is cached when it shouldn't be. Why is that? Because this request is attempting to use a disallowed authentication provider, yet Page Cache is still trying to cache it. \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests prevents this from happening for basic_auth and \Drupal\Core\PageCache\RequestPolicy\NoSessionOpen prevents this from happening for cookie. So why is this not happening here? Because the REST test authentication providers do not yet have a page cache request policy denying them from being cached!

Simple fix :)

Wim Leers’s picture

Title: Make HTTP Exceptions Cacheable » Make anonymous 4xx REST responses cacheable by PageCache (or any other reverse proxy)
Issue tags: -Needs title update +Performance, +scalability

We still need to update the IS, but here's already an improved title. The current title keeps confusing me.

Status: Needs review » Needs work

The last submitted patch, 151: rest_cacheable_response-2765959-151.patch, failed testing.

Wim Leers’s picture

The remaining test failures are for testing GET on the User entity type, only when using the hal_json format, with the following failures:

  1. UserHalJsonBasicAuthTest: Failed asserting that 401 is identical to 403. (403 expected, 401 instead) on line 379 of EntityResourceTestBase, due to a Dynamic Page Cache hit that shouldn't be a Dynamic Page Cache hit. Most notably, there are no cache contexts associated with that cached 401 response!
  2. UserHalJsonAnonTest: Failed asserting that 403 is identical to 200. (200 expected, 403 instead) on line 389 of EntityResourceTestBase. This is neither a Page Cache hit nor a Dynamic Page Cache hit.
  3. UserHalJsonCookieTest: Failed asserting that 403 is identical to 200. (200 expected, 403 instead) on line 389 of EntityResourceTestBase, due to a Dynamic Page Cache hit that shouldn't be a Dynamic Page Cache hit. Most notably, there are no cache contexts associated with that cached 403 response!

The question is of course: why?. Why just this entity type, and why only in the hal_json format?

Investigating…

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
1.12 KB
32.62 KB

Root cause for failing UserHalJsonAnonTest found: missing cache context in one of the cases in \Drupal\user\UserAccessControlHandler::checkAccess(). Which resulted in zero cache contexts associated with the access denied exception, and hence inappropriate caching.

Of course, that begs the question: why did UserJsonAnonTest not also fail?

This took some further digging. Turns out the root cause is \Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on4xx(). This runs with priority -75. So the json format's 4xx responses are handled by this one.

Contrast this with \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber::on4xx(), which handles all serialization formats. It also has priority -75. But because it happens to be ordered before the aforementioned one despite the identical weight, this one did not handle the json format. But it did handle the hal_json format.

In other words… UserJson*Test should also have failed. But it didn't. So let's first show that we can make things fail consistently, for all User tests. Then we can confirm that the root cause (\Drupal\user\UserAccessControlHandler::checkAccess()) in fact did apply in all cases.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

I'm awaiting the test result of #155. If all goes well, then we'll have 6 failures:

  1. UserHalJsonAnonTest
  2. UserHalJsonBasicAuthTest
  3. UserHalJsonCookieTest
  4. UserJsonAnonTest
  5. UserJsonBasicAuthTest
  6. UserJsonCookieTest

I will post the fix and next steps as soon as #155 indeed comes back with 6 failures. Please don't work on this issue in the mean time. It'll make this issue even harder to follow.

Status: Needs review » Needs work

The last submitted patch, 155: rest_cacheable_response-2765959-155.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
898 bytes
33.46 KB

Alright, #155 had the 6 expected failures. Now we're in a consistent state. So now let's apply the bugfix I mentioned at the beginning of #155.

Wim Leers’s picture

#158 should come back green. Time to do a proper review.

  1. +++ b/core/lib/Drupal/Core/Cache/Exception/CacheableBadRequestHttpException.php
    @@ -0,0 +1,22 @@
    +/**
    + * A Bad Request exception that contains and can expose cacheability metadata.
    + *
    + * Supports Drupal's caching concepts: cache tags for invalidation and cache
    + * contexts for variations.
    + *
    + * @see \Drupal\Core\Cache\Cache
    + * @see \Drupal\Core\Cache\CacheableMetadata
    + * @see \Drupal\Core\Cache\CacheableResponseTrait
    + */
    +class CacheableBadRequestHttpException extends BadRequestHttpException implements CacheableHttpExceptionInterface  {
    

    This, repeated for every single of these new classes… that makes for a fairly lengthy docblock. But I think it's warranted, because without this, it may end up confusing users.

    (There are a bunch of nits to be fixed in all of these though.)

  2. +++ b/core/lib/Drupal/Core/Cache/Exception/CacheableBadRequestHttpException.php
    @@ -0,0 +1,22 @@
    +  use CacheableResponseTrait;
    

    However, it's rather strange to see this use CacheableResponseTrait for an exception. So let's at least update the docs for that trait. Because it does make sense to use this trait here.

  3. +++ b/core/lib/Drupal/Core/Entity/Entity.php
    @@ -527,10 +527,8 @@ protected function invalidateTagsOnSave($update) {
    -    if ($this->hasLinkTemplate('canonical')) {
    -      // Creating or updating an entity may change a cached 403 or 404 response.
    -      $tags = Cache::mergeTags($tags, ['4xx-response']);
    -    }
    +    // Creating or updating an entity may change a cached 403 or 404 response.
    +    $tags = Cache::mergeTags($tags, ['4xx-response']);
    
    +++ b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php
    @@ -392,11 +392,13 @@ public function testPostSave() {
    +        '4xx-response',
    ...
    +        '4xx-response',
    

    I think this belongs in a separate issue.

    This needs explicit test coverage of its own and the update to EntityUnitTest::testPostSave() does provide that.

    But what is missing here, is proof that we really need this. i.e. what's missing, is test coverage showing that a certain scenario fails unless we make these changes.

    #108.1 said: For instance, if you create a view that retrieves an entity at say /awesome/{id} that id will 404 which will be cached, creating the entity at that id should clear it out. That all works with entities that have a canonical link template, but if it doesn't, it doesn't purge them and there isn't an easy way to do it unless you add it to every entity that doesn't have the canonical link template. … but the recently added test coverage for Item at #2843752: EntityResource: Provide comprehensive test coverage for Item entity (which is a content entity type without a UUID field) still passes without this AFAICT. So that suggest we don't need this.

  4. +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
    @@ -158,6 +160,11 @@ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $st
    +      // Persist any cacheability metadata.
    
    +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionSubscriber.php
    @@ -118,7 +120,15 @@ protected function onHtml(GetResponseForExceptionEvent $event) {
    +    // Persist any cacheability metadata.
    
    +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
    @@ -35,7 +35,16 @@ protected static function getPriority() {
    +    // Persist any cacheability metadata.
    
    +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
    @@ -130,6 +132,14 @@ public function challengeException(Request $request, \Exception $previous) {
    +    // Persist any cacheability metadata.
    
    +++ b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
    @@ -67,14 +74,22 @@ public function on4xx(GetResponseForExceptionEvent $event) {
    +    // Persist any cacheability metadata.
    

    A response is being sent for an exception. The response must inherit the exception's cacheability metadata.

  5. +++ b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php
    @@ -77,7 +77,7 @@ protected function copyRawVariables(array $defaults) {
    -      $event->setException(new NotFoundHttpException($exception->getMessage(), $exception));
    +      $event->setException(new CacheableNotFoundHttpException($exception->getMessage(), $exception));
    

    This is not associating any cacheability metadata. Which means it's unsafe to make this exception cacheable.

    This should be done in a separate issue, with explicit test coverage.

  6. +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
    @@ -130,6 +132,14 @@ public function challengeException(Request $request, \Exception $previous) {
    +      $exception = new CacheableUnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
    ...
         return new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
    

    We can prevent this duplication, by first creating the non-cacheable exception, then conditionally converting it to a cacheable one.

  7. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -122,7 +123,10 @@ public static function create(ContainerInterface $container, array $configuratio
    +      $exception->addCacheableDependency($entity);
    +      $exception->addCacheableDependency($entity_access);
    

    We should only have to add $entity_access as a cacheable dependency.

    If $entity affects the AccessResult, then it's up to the access checking logic to add $entity as a cacheable dependency to $entity_access.

Wim Leers’s picture

This addresses #159.1 + #159.2. This:

  1. removes excessively detailed/distracting class docblocks from all the new CacheableHttp*Exception classes
  2. makes CacheableResponseInterface point to CacheableHttpExceptionInterface and vice versa, to clearly establish their link by similarity
  3. updates the documentation in CacheableResponseTrait
  4. updates CacheableHttpExceptionInterface to not reuse CacheableResponseInterface — reusing that interface means that all cacheable HTTP exceptions implement that interface, which could lead to if-tests that jump to the wrong conclusions and hence treat exceptions as responses!

This has zero functional changes compared to #158, and should therefore have identical test results.

tstoeckler’s picture

+++ b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php
@@ -77,7 +77,7 @@ protected function copyRawVariables(array $defaults) {
     if ($exception instanceof ParamNotConvertedException) {
-      $event->setException(new NotFoundHttpException($exception->getMessage(), $exception));
+      $event->setException(new CacheableNotFoundHttpException($exception->getMessage(), $exception));
     }

Why is a failed parameter conversion cacheable? Seems like if I have 3 nodes and then visit /node/4 anonymously and then create another one, the 404 should not be cached? What am I missing?

Wim Leers’s picture

#161: I agree, see #159.5. See @davidwbarratt's earlier comments wrt ParamConversionEnhancer, he's the one who wrote this. I intend to remove that hunk for sure.

tstoeckler’s picture

Ahh sorry, missed that. Thanks for the confirmation!

davidwbarratt’s picture

A ParamConversionEnhancer exception is a cachable exception in HTML:

$ curl --head https://sailvenice.com/node/12345
HTTP/1.1 404 Not Found
Date: Wed, 05 Apr 2017 11:16:47 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/5.6.18-1+deb.sury.org~precise+1
Cache-Control: max-age=86400, public
X-Drupal-Dynamic-Cache: HIT
Link: </node/1>; rel="canonical"
Link: </node/1>; rel="shortlink"
Link: </node/1>; rel="revision"
Link: </node/1>; rel="canonical"
Link: </node/1>; rel="shortlink"
Link: </node/1>; rel="revision"
Link: </node/1>; rel="canonical"
Link: </node/1>; rel="shortlink"
Link: </node/1>; rel="revision"
Link: </node/1>; rel="canonical"
Link: </node/1>; rel="shortlink"
Link: </node/1>; rel="revision"
X-UA-Compatible: IE=edge
Content-language: en
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
Last-Modified: Wed, 05 Apr 2017 11:16:45 GMT
Vary: Cookie,Accept-Encoding
X-Generator: Drupal 8 (https://www.drupal.org)
Purge-Cache-Tags: 4xx-response block_content:1 block_content:2 block_content:3 block_content:6 block_content_view block_view config:block.block.aboutus config:block.block.copyright config:block.block.followus config:block.block.mainnavigation config:block.block.messages config:block.block.needhelp config:block.block.rateus config:block.block.sitebranding config:block.block.tabs_2 config:block_list config:filter.format.basic_html config:filter.format.plain_text config:filter.format.restricted_html config:system.menu.main config:system.menu.need-help config:system.site config:user.role.anonymous node:1 node:4 node:5 node:6 node:7 node_view rendered
X-Varnish: 1283440 170259
Age: 2
Via: 1.1 varnish-v4
X-Cache: HIT
X-Cache-Hits: 1

I think the API should match this behaviour.

Let's say you had 1,000/requests/minute going to /node/2 and then you deleted it. At that moment, you'd go from having no requests for that node (since it's cached) to 1,000/requests/minute and there would be no way to stop it. :(

Also, if you hadn't yet created /node/4 that should be a cachable 404 until you create it, at which point it should be purged from the cache (which is exactly what happens with HTML).

I don't really care the method in which we get to this, but I think public 404's, regardless of route or format, should be cacheable.

Status: Needs review » Needs work

The last submitted patch, 160: rest_cacheable_response-2765959-160.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
868 bytes
30.97 KB

Ahhh, finally! After endless DrupalCI infra problems, we now have the test results for both #158 and #160! They're both green, as expected :)

This then addresses #159.7.


@davidwbarratt: welcome back! I hope you're glad to see that your work has not been in vain! I'm currently working to address my review points in #159. To address #159.5 and your explanation in #164, I will simply remove your changes in a reroll, and observe what failures it causes. Then we'll take it from there. I'm not per se opposed to it, but we need to make sure that it's carefully tested, so that we don't accidentally break other things, or accidentally introduce security problems. I'm sure you agree with that.

Status: Needs review » Needs work

The last submitted patch, 167: rest_cacheable_response-2765959-167.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
5.57 KB
31.52 KB

As predicted, #167 is green.

This then performs the next step: fixing #159.4 (fixing comments) and #159.6 (reducing duplication/brittleness in BasicAuth changes). These should be non-functional changes, so the patch should still come back green.


Once this patch comes back green, we'll finally be able to address #159.3 (Entity changes) and #159.5 (ParamConversionEnhancer changes). I will remove both of those changes, one-by-one, so we can observe the test failures (if any). Ideally, we'd be able to move those changes to separate issues.

Wim Leers’s picture

Still green, great.

Let's observe what happens if we revert #159.3.

Wim Leers’s picture

#170's test run is still in progress, but all HAL and most REST tests already came back green. So I'm pretty sure #170 will come back green.

So now let's observe what happens if we revert #159.5 as well.

dawehner’s picture

Here is maybe a naive question. On block access (\Drupal\block\BlockRepository::getVisibleBlocksPerRegion) we need to take into account every possible combination of access to determine the cacheability metadata. Doesn't that mean we have to do the same for any access checking? I guess throwing exceptions is conceptually wrong (Note: In d7 we essentially did the same with drupal_access_denied()), and this issue doesn't cause more problems, given we do the same for normal routing already.

tstoeckler’s picture

Re #164: Thanks for that list, that reminded of the 4xx-response which does "fix" exactly what I thought would break in #161 because it is automatically cleared on every entity save. So that somewhat answers question, although I can't really comment on the patch as a whole, that's a bit above my head.

Wim Leers’s picture

So now we reverted #159.3 (in #170) and #159.5 (in #171). My review in #159 is now fully addressed!

However, @davidwbarratt added them to ensure we get cacheable 404 responses. We don't know if 404 responses are still cacheable after removing those hunks, because… @davidwbarratt had not yet added test coverage. That's also why I think it shouldn't surprise anybody that #170 and #171 came back green.

So, let's work on adding that test coverage. If we then notice that 404 responses that we'd like to be cached are not cached, we can still choose to restore those hunks. Or we can choose to defer that to a follow-up — this issue would then be responsible for

  1. introducing the necessary infrastructure
  2. making all the exceptions that the REST module throws that result in 4xx responses cacheable
  3. having explicit test coverage for cache tags + cache contexts + Page Cache response header value + Dynamic Page Cache response header value for every 4xx response in EntityResourceTestBase

If some of those 404 responses then are not cacheable, we would know about it, and it'd be much simpler to implement #159.3+#159.5 in a follow-up issue.


Finally, I'd say this involves a security risk, because missing cache contexts can cause information disclosure. But in this case, that's not true. If you get a 4xx response that you should not get, you're just being told the reason why you're being denied access. None of that is sensitive information. The worst that can happen, is that you're getting an incorrect 4xx response, and that it hence is worsening the DX. However, when such a case is encountered, we should simply add test coverage to EntityResourceTestBase and fix it. And the massive amount of test coverage this needs to add per the above would already be a strong guarantee that it's correct.


This test coverage is what I've been working on today. Many tests are passing, but not yet all. I want to get to a better place before posting a patch here. Stay tuned — definitely expect a patch tomorrow :)

Wim Leers’s picture

Status: Needs review » Needs work

Per #174.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
5.83 KB
33.9 KB

So! Time to get that test coverage going! This took a lot of work. But it will result in an update for all test coverage added by #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, and hence all that REST test coverage the all also asserting all cacheability aspects. This is hugely important for the future.

To get started: let's expand ResourceTestBase::assertResource(Error)Response(): let's update it to test all four things mentioned in #174:

having explicit test coverage for cache tags + cache contexts + Page Cache response header value + Dynamic Page Cache response header value for every 4xx response in EntityResourceTestBase

We'll let this by default assert that a response doesn't have a X-Drupal-Cache-Tags header, nor a X-Drupal-Cache-Contexts header, nor a X-Drupal-Cache header nor a X-Drupal-Dynamic-Cache header. In other words: by default, all REST reponse assertions will check that the responses are not cacheable at all.

As you'll see in the test results, this will already allow the testPost(), testPatch() and testDelete() test coverage to mostly work. That makes sense, because POST/PATCH/DELETE are "unsafe" HTTP methods.

Wim Leers’s picture

testGet() for basic_auth and cookie are now failing on the assertions in assertResponseWhenMissingAuthentication(), because we need to update those. In case of:

  1. cookie, we're just seeing the 403 response thrown by entity access denying us access in EntityResource::get(), and so we need to expect that that 403 response is now cacheable
  2. basic_auth, we have a similar problem as for cookie… but I realized that in fact it doesn't make sense to inherit the cacheability metadata of the 403 when we convert that 403 to a 401. In fact, it makes total sense to simply let basic_auth always generate a cacheable 401. Detailed explanation in a code comment in \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException(). Lots of explicit test coverage for various situations thanks to the REST test coverage.
Wim Leers’s picture

Now e.g. NodeJsonBasicAuthTest and NodeJsonCookieTest are failing a bit further: it's time we update a whole bunch of the assertResource(Error)Response() assertions, because the default assumption (as explained in #176) that those responses are not cacheable at all is not true for responses to GET requests, of course.

As you can see in the interdiff, we did already have test coverage for cacheability of a successful 200 response. But that was the only test coverage we had for this. We're now changing that to have all received responses be tested for their cacheability!

Wim Leers’s picture

Yay, the REST testGet() test coverage for Node is passing again! :)

But … turns out that some entity types' access control handlers deviate from what's true in most cases: some have the cacheability depend on the entity in question (for example: access is granted only if the entity is published, or not locked), which then of course means that that entity's cache tag is present on the entity access result's cacheability metadata, which then is also present in the access result.

Therefore we add a new \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::getExpectedUnauthorizedAccessCacheability() method, and have those entity types that need it override that default implementation in their test base class.

Wim Leers’s picture

Now almost all testGet() tests are passing!

In fact, quite a lot is passing completely. Except for testDelete(), many of those seem to be failing at the same point.

Wim Leers’s picture

Yay, lots of green.

Now let's update the User-specific edge case test coverage in \Drupal\Tests\rest\Functional\EntityResource\User\UserResourceTestBase::testPatchDxForSecuritySensitiveBaseFields().

Wim Leers’s picture

#2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster contains a fix for \Drupal\system\EventSubscriber\AdminRouteSubscriber that we also need here: otherwise the Shortcut, MenuLinkContent and Block entities are treated differently by Dynamic Page Cache, because their paths begin with the prefix /admin. And that causes those to behave differently than the other entity types.

Wim Leers’s picture

Some particularly nasty work-arounds are necessary for the strangeness going on in \Drupal\block\BlockAccessControlHandler::mergeCacheabilityFromConditions()() — well, really, in the context system.

Wim Leers’s picture

And then finally, an oversight in the cacheability metadata of REST responses: the config:rest.settings cache tag. This should have been added to the cacheability metadata in #2664780: Remove REST's resource- and verb-specific permissions for EntityResource, but provide BC and document why it's necessary for other resources, but it wasn't, because REST test coverage was not yet asserting cacheability of all responses. As of this patch we are, and so now we are seeing that this is a problem… at least in case of the Block entity.

Another WTF for developers fixed/avoided!

If all goes well, this patch should then finally be green!

We now have comprehensive REST response cacheability test coverage! Whew.

Wim Leers’s picture

Title: Make anonymous 4xx REST responses cacheable by PageCache (or any other reverse proxy) » Make anonymous 4xx REST responses cacheable by PageCache (or any other reverse proxy) — and ensure every tested REST resource response's cacheability behaves as expected
Priority: Normal » Major

If #176 through #184 doesn't convince you this is major, then I don't know what will.

davidwbarratt’s picture

YAY! Thank you @wim-leers!

Wim Leers’s picture

Status: Needs review » Postponed

Per #182.

Wim Leers’s picture

#2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage was committed, here's a reroll per #182.

Changes:

This conflicted with #2853300: Standardize fatal error/exception handling: backtrace for all formats, not just HTML
… which renamed Drupal\Core\EventSubscriber\DefaultExceptionSubscriber to Drupal\Core\EventSubscriber\FinalExceptionSubscriber. The actual diff remained the same though, it just needed to be applied to a different file.
It also conflicted with #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster.
This conflict resolution is shown in rebase-interdiff.txt.
Finally, it also included the _admin_route-related changes …
… that #2827797 used to contain, but which were split off to separate issues: #2874938: AdminRouteSubscriber must only mark HTML routes as administrative + #2877528: Change Dynamic Page Cache's response policy from "deny admin routes" to "deny html admin routes". So, I reverted the changes this patch was making to \Drupal\system\EventSubscriber\AdminRouteSubscriber. However, a consequence is that this then requires the same temporary "if-else" work-around that existed in a few places in #2827797 to be introduced in many places in this patch (from 2 to 8).
This resolution is shown in interdiff.txt.
Wim Leers’s picture

To be clear: #188 could be massively simplified if either #2874938: AdminRouteSubscriber must only mark HTML routes as administrative or #2877528: Change Dynamic Page Cache's response policy from "deny admin routes" to "deny html admin routes" land, or both. Then most of the changes in ##18's interdiff could be removed. Patch size would shrink roughly 8 KB.

Status: Needs review » Needs work

The last submitted patch, 188: rest_cacheable_response-2765959-188.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
1.64 KB
65.18 KB

I made a small mistake in #188 when splitting this up in the two interdiffs. Easy fix fortunately :)

tedbow’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll

Needs reroll

Wim Leers’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
65.84 KB

Rerolled. Conflicted with #2877778: Harden test EntityResource + Dynamic Page Cache test coverage, which hardened some test coverage that this patch moves around. Had to update this patch to move the updated code around instead. Effectively a straight rebase.

I'll work on an updated issue summary in the coming days.

Wim Leers’s picture

The hardening that #2877778: Harden test EntityResource + Dynamic Page Cache test coverage did worked well, but this patch breaks the assumptions that were made. The assumption that was made was that only 200 responses would ever be cached. Therefore, it assumed 2 cache items in the cache_dynamic_page_cache DB table: a cache redirect, and the actual cached response.

However, this issue changes that: this issue makes 4xx responses cacheable too. Therefore it is possible to have >2 cache items: there may be cached 4xx responses.

So let's update the patch to check that:

  1. there are >=2 cache items
  2. of which 1 must be a cache redirect
  3. of which 1 must be a cached 200 response
  4. and all other cache items — if there are any — must be cached 4xx responses

That still maintains the spirit of #2877778: Harden test EntityResource + Dynamic Page Cache test coverage.

Wim Leers’s picture

In the mean time, #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX also landed, which added additional test coverage. It also was asserting a 200 response. In this patch, we're making it required to check (Dynamic) Page Cache HIT/MISS and cache tags + contexts. So, let's update the assertion it introduced.

The last submitted patch, 193: rest_cacheable_response-2765959-193.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 194: rest_cacheable_response-2765959-194.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Wim Leers’s picture

Fixing coding standards violations in #195, using testbot's codesniffer_fixes.patch.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Issue summary: View changes
Issue tags: -Needs issue summary update +API addition

IS rewritten. CR created: https://www.drupal.org/node/2884481.

This is ready for final review.

Wim Leers’s picture

Title: Make anonymous 4xx REST responses cacheable by PageCache (or any other reverse proxy) — and ensure every tested REST resource response's cacheability behaves as expected » Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage

Better title.

tedbow’s picture

Status: Needs review » Needs work

#159.2
So yes it seems weird to use CacheableResponseTrait in exceptions. The documenation was updated and it was noted so if no one else has problem with that I won't hold it up.

I guess it we can't rename this trait?

  1. +++ b/core/lib/Drupal/Core/Cache/Exception/CacheableHttpExceptionInterface.php
    @@ -0,0 +1,40 @@
    +   * For instance, when a response depends on some configuration, an entity, or
    +   * an access result, we must make sure their cacheability metadata is present
    +   * on the response. This method makes doing that simple.
    ...
    +   * Returns the cacheability metadata for this response.
    

    Comments for this interface still refer to responses though this is an interface for expections. Probably just copy/pass error from CacheableResponseInterface

  2. +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
    @@ -352,17 +365,72 @@ protected function request($method, Url $url, array $request_options) {
    +      if ($expected_status_code < 400) {
    +        $this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
    +      }
    +      else {
    +        $this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
    +      }
    

    In this if/else we make the same exact call to assertSame() in both cases.

    Not sure what the intention was but that can't be right 😜

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
66.96 KB
2.42 KB

RE #159.2: No, we can't rename this trait. That'd be a BC break. What we can do is this:

Before
trait CacheableResponseTrait {
  protected $cacheabilityMetadata;
  public function addCacheableDependency($dependency) { …}
  public function getCacheableMetadata() { … }
}
After
trait CacheableResponseTrait {
  use CacheabilityCarryingObjectTrait;
}
trait CacheableExceptionTrait {
  use CacheabilityCarryingObjectTrait;
}
trait CacheabilityCarryingObjectTrait {
  protected $cacheabilityMetadata;
  public function addCacheableDependency($dependency) { …}
  public function getCacheableMetadata() { … }  
}
  1. Well-spotted! Fixed.
  2. Wow, amazing find! This was a pre-existing problem though, but yes, when touching this code, it makes sense to clean it up. Much better obviously :)
dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Cache/Exception/CacheableUnauthorizedHttpException.php
    @@ -0,0 +1,15 @@
    +namespace Drupal\Core\Cache\Exception;
    

    I know its status quo already, but its a bit sad that the cache component also deal with http caching. It feels like a better approach would separate that, but nevermind, just throwing out my thought here.

  2. +++ b/core/lib/Drupal/Core/EventSubscriber/FinalExceptionSubscriber.php
    @@ -125,7 +127,15 @@ public function onException(GetResponseForExceptionEvent $event) {
     
         $content = $this->t('The website encountered an unexpected error. Please try again later.');
         $content .= $message ? '</br></br>' . $message : '';
    -    $response = new Response($content, 500, ['Content-Type' => 'text/plain']);
    +
    +    // If the exception is cacheable, generate a cacheable response.
    +    if ($exception instanceof CacheableHttpExceptionInterface) {
    +      $response = new CacheableResponse($content, 500, ['Content-Type' => 'text/plain']);
    +      $response->addCacheableDependency($exception->getCacheableMetadata());
    +    }
    +    else {
    +      $response = new Response($content, 500, ['Content-Type' => 'text/plain']);
    +    }
    

    I'm sorry, but at least for me it never makes sense to cache a 500. This feels like unnecessary code complexity to be honest.

  3. +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
    @@ -130,7 +131,32 @@ public function challengeException(Request $request, \Exception $previous) {
    +    // 1. vary by whether the current user has the 'anonymous' role or not. This
    +    //    works fine because:
    +    //    - Page Cache never caches a response whose request has Basic Auth
    +    //      credentials thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests.
    +    //    - Dynamic Page Cache will cache a different result for when the
    +    //      request is unauthenticated (this 401) versus authenticated (some
    +    //      other response)
    +    // 2. have the 'config:user.role.anonymous' cache tag, because the only
    +    //    reason this 401 would no longer be a 401 is if permissions for the
    +    //    'anonymous' role change, causing that cache tag to be invalidated.
    ...
    +    $exception = new CacheableUnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
    

    Maybe a naive question: The message changes depending on the system.site configuration. Does this mean it should be part of the cache tags?

  4. +++ b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
    @@ -31,8 +31,20 @@ protected function getAuthenticationRequestOptions($method) {
    +    if ($method === 'GET' && strpos($this->entity->getEntityType()->getLinkTemplate('canonical'), '/admin/') !== 0) {
    +      $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:user.role.anonymous', 'http_response'], ['user.roles:anonymous'], $expected_page_cache_header_value, $expected_page_cache_header_value);
    +    }
    

    What about simply just calculating the false variable of assertResourceErrorResponse. Then we could basically get rid of the if/else and probably replace it by an easy to read ?:

Wim Leers’s picture

Assigned: Unassigned » Wim Leers
Issue tags: +API-First Initiative

#203: thanks!

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Issue summary: View changes
FileSize
4.58 KB
65.75 KB

#203:

  1. Would you like to see this moved from core/lib/Drupal/Core/Cache/Exception/ to core/lib/Drupal/Core/Http/Exception/? I think that would make sense!
  2. Excellent point! I should've realized this when I took over this issue/patch.
  3. Good catch, fixed! And the changes are trivial, fortunately. It's nice to see that "forgot this bit of cacheability metadata" results in trivial fixes :)
  4. I opted to do it this way because that'll make it easy to change when either of the two blocker issues are fixed: because then it's just a matter of removing the if/else + docblock and decreasing the indentation. I In this particular case it could indeed work, but in EntityResourceTestBase, there are similar if/else statements where the if-case contains a lot more logic. Having consistent if/else statements makes it easy/low-risk to clean this up when either of those 2 issues land.

Addressed points 2 and 3. I disagree on point 4. I'm happy to address point 1 if @dawehner likes my suggestion :)

dawehner’s picture

Would you like to see this moved from core/lib/Drupal/Core/Cache/Exception/ to core/lib/Drupal/Core/Http/Exception/? I think that would make sense!

... I was really mostly loudly thinking load. I'm glad someone else has the same opinion :)

I opted to do it this way because that'll make it easy to change when either of the two blocker issues are fixed: because then it's just a matter of removing the if/else + docblock and decreasing the indentation. I In this particular case it could indeed work, but in EntityResourceTestBase, there are similar if/else statements where the if-case contains a lot more logic. Having consistent if/else statements makes it easy/low-risk to clean this up when either of those 2 issues land.

Fair, I don't worry much to be honest :)

Wim Leers’s picture

:)

Done!

BTW, what do you think about the proposal in #202?

dawehner’s picture

BTW, what do you think about the proposal in #202?

I'm wondering whether having simply a better name would be a better approach. This would then be in total 2 traits (the existing one + the better name) instead of 3, which is your proposal in #202.

The last submitted patch, 205: rest_cacheable_response-2765959-205.patch, failed testing. View results

Wim Leers’s picture

That could work too. So then what do you think about what I wrote in #202: CacheabilityCarryingObjectTrait?

tedbow’s picture

I would be in favor of @dawehner's idea in #208 adding just CacheabilityCarryingObjectTrait and not CacheableExceptionTrait
and then maybe deprecating CacheableResponseTrait because it could be replaced by CacheabilityCarryingObjectTrait

Wim Leers’s picture

#211: Yep, we'd then indeed deprecate CacheableResponseTrait. Do you approve of the name?

If @dawehner can confirm he also likes the name, I'll get it done.

dawehner’s picture

#211: Yep, we'd then indeed deprecate CacheableResponseTrait. Do you approve of the name?

WFM

Status: Needs review » Needs work

The last submitted patch, 207: rest_cacheable_response-2765959-207.patch, failed testing. View results

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

First, let's get this stable. Then, I'll implement #211, because it has both @tedbow's and @dawehner's approval.

Wim Leers’s picture

+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -155,6 +156,7 @@ public function challengeException(Request $request, \Exception $previous) {
+    $exception->addCacheableDependency($site_config);

This change in #205 is what caused the test failures. Because it adds the languages:language_interface cache context, since the system.site config may vary by interface translation.

Working on a fix.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
6.82 KB
72.05 KB

The thing #216 didn't explain yet, is that it only does that for ConfigurableLanguage*BasicAuthTest and ContentLanguageSettings*BasicAuthTest because they both install the language module. Annoying! But correct.

The solution is fortunately not very hard: an alternative trait for BasicAuthResourceTestTrait for those tests that do install the language module.

This should be green again, but I still need to implement #211.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Wim Leers’s picture

Time to continue here again. Finally.

This conflicted pretty heavily with #2869387: Subclasses of ResourceTestBase for non-entity resources are required to add pointless code. First rebasing.

Status: Needs review » Needs work

The last submitted patch, 219: rest_cacheable_response-2765959-219.patch, failed testing. View results

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
1.93 KB
73.79 KB

Those 12 failures only exist because more REST test coverage has been committed since June. Two of those newly committed pieces of test coverage (Media and BlockContent) obviously associate cacheability metadata with their "access denied" access result.

This should be green again.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
FileSize
31.62 KB
77.66 KB

Implemented #215 (which is #202 + #208 + #210 + #211 + #212 + #213). Except I've simplified it even further compared to what @dawehner asked for: rather than adding a new interface and trait, I'm instead changing things so that we just reuse CacheableDependencyInterface. This means that the cacheable HTTP exceptions become immutable rather than mutable, which seems far safer. On top of that, it means fewer interfaces, and more consistency with the rest of core!

IOW:
extract a CacheableDependencyTrait from RefinableCacheableDependencyTrait
remove CacheableHttpExceptionInterface and CacheableHttpException (the base class)
instead, let cacheable HTTP exceptions implement the already-existing-and-widely-implemented CacheableDependencyInterface, and to avoid excessive repetition in the implementations, use CacheableDependencyTrait
remove all changes to CacheableResponseInterface and CacheableResponseTrait

borisson_’s picture

Status: Needs review » Needs work

Nitpicks ahead:

  1. +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
    @@ -126,11 +127,34 @@ public function authenticate(Request $request) {
    +    //    - Page Cache never caches a response whose request has Basic Auth
    +    //      credentials thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests.
    

    80 cols by putting the class on the next line?

  2. +++ b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
    @@ -31,8 +33,20 @@ protected function getAuthenticationRequestOptions($method) {
    +    if ($method === 'GET' && strpos($this->entity->getEntityType()->getLinkTemplate('canonical'), '/admin/') !== 0) {
    +      $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['user.roles:anonymous'], $expected_page_cache_header_value, $expected_page_cache_header_value);
    +    }
    +    else {
    +      $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['user.roles:anonymous'], $expected_page_cache_header_value, FALSE);
    +    }
    

    I understand that we need a difference here, but it looks like the only thing that is different is: $expected_dynamic_page_cache_header_value in the second calls is always false the second time.

    Do you think we can make this is there a way to make this more explicit?

    if (...) { 
    $expected_dynamic_page_cache_header_value = $expected_page_cache_header_value
    }
    else {
    $expected_dynamic_page_cache_header_value = FALSE
    }
    $this->assertResourceErrorReposponse(..., $expected_page_cache_header_value, expected_dynamic_page_cache_header_value);
    

    I think that would make the reason for this if clearer.

  3. +++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
    @@ -91,11 +91,41 @@ protected function getAuthenticationRequestOptions($method) {
    +      // @todo Entity resources with URLs that begin with '/admin/' are marked as
    

    80 cols

  4. +++ b/core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php
    @@ -141,4 +141,19 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
    +    return parent::getExpectedUnauthorizedAccessCacheability()
    +      // @see \Drupal\block\BlockAccessControlHandler::checkAccess()
    +      ->setCacheTags([
    

    Can we put the comment above the start of the code? At first glance it looked like part of this code was commented out.

  5. +++ b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php
    @@ -171,4 +171,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
    +    return parent::getExpectedUnauthorizedAccessCacheability()
    +      // @see \Drupal\block_content\BlockContentAccessControlHandler()
    +      ->addCacheTags(['block_content:1']);
    

    Same here.

  6. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -515,7 +523,17 @@ public function testGet() {
    +      // @todo Entity resources with URLs that begin with '/admin/' are marked as
    +      //   administrative (see https://www.drupal.org/node/2874938), which
    

    80 cols

  7. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -549,7 +567,17 @@ public function testGet() {
    +      // @todo Entity resources with URLs that begin with '/admin/' are marked as
    +      //   administrative (see https://www.drupal.org/node/2874938), which
    

    80 cols

  8. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -585,7 +613,30 @@ public function testGet() {
    +    // @todo Fix \Drupal\block\BlockAccessControlHandler::mergeCacheabilityFromConditions() in https://www.drupal.org/node/2867881
    

    80 cols.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
11.95 KB
77.62 KB

#223: thanks so much for the review! ❤️

  1. 👍 Done.
  2. 👍 Done.
  3. 👍 Done.
  4. 👍 I felt this was clearer, because the comment only applies to the changes made on top of what the parent method returns. But I see your point: it's atypical. Done.
  5. 👍 Done. And several more of them.
  6. 👍 Done.
  7. 👍 Done.
  8. 👍 Done.
borisson_’s picture

Awesome, just one question, this is tagged as performance and scalability but it doesn't have any numbers on the performance improvements made. Should we get those to prove that this actually makes things better?

Wim Leers’s picture

See the issue summary created by the original reporter: (s)he was reporting that 4xx responses for REST resources were not being cached. This patch makes those responses cacheable. Test coverage proves this, and provides has the correct cacheability metadata. Which improves scalability. It also means a repeated request for the same URL will result in a faster 4xx response, much like #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster. That issue made authenticated REST responses 15% faster (on a simple use case — 15% is pretty much the minimal expected performance gain).

borisson_’s picture

The reason why I asked is because I wanted to see if this would also mean a 15% increase or if it was lower/higher. I guess it doesn't really matter because we're sure that this is a performance improvement anyway.

I think this issue also means that we should reroll #2472335: Support an independent max-age for 4xx responses and make rest responses also use that configuration.

I read trough the entire patch again and couldn't find anything that i'd change, but leaving this for someone more knowledgable to rtbc.

Wim Leers’s picture

I think you're qualified enough to RTBC this 😉

(Also: reviewed #2472335: Support an independent max-age for 4xx responses . There's nothing that patch needs to change to support this AFAICT. But once this issue lands, we can let #2472335: Support an independent max-age for 4xx responses add explicit REST test coverage in EntityResourceTestBase, and then we'll have absolute certainty!)

Wim Leers’s picture

This is a pretty big patch, one that understandably will take quite some time to get reviewed & approved. So I'm looking at how we could break this patch up. (That doesn't mean this patch can't reach RTBC in its current state: it'd just become a fair bit smaller if this other patch lands first!)

  1. --- /dev/null
    +++ b/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php
    --- a/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php
    +++ b/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php
    

    I could extract these changes into a separate issue, but nothing would use this new trait.

  2. --- /dev/null
    +++ b/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php
    --- /dev/null
    +++ b/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php
    --- /dev/null
    +++ b/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php
    --- /dev/null
    +++ b/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php
    

    I could extract these (and all other similar ones) to a separate issue, but nothing would use these new exceptions.

    (The exceptions would use the trait though.)

  3. +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
    @@ -170,6 +172,11 @@ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $st
    +      if ($response instanceof CacheableResponseInterface && $exception instanceof CacheableHttpExceptionInterface) {
    +        $response->addCacheableDependency($exception->getCacheableMetadata());
    +      }
    
    +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
    @@ -35,7 +38,16 @@ protected static function getPriority() {
    +    if ($exception instanceof CacheableDependencyInterface) {
    +      $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
    +      $response->addCacheableDependency($exception);
    +    }
    

    However, the two bullets above, together with these changes … actually could end up being a sensible patch to extract from this issue.

  4. … and upon looking into it further…
    +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
    @@ -126,11 +127,35 @@ public function authenticate(Request $request) {
    +    // A 403 is converted to a 401 here, but it doesn't matter what the
    +    // cacheability was of the 403 exception: what matters here is that
    +    // authentication credentials are missing, i.e. that this request was made
    +    // as the anonymous user.
    +    // Therefore, all we must do, is make this response:
    +    // 1. vary by whether the current user has the 'anonymous' role or not. This
    +    //    works fine because:
    +    //    - Thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests,
    +    //      Page Cache never caches a response whose request has Basic Auth
    +    //      credentials.
    +    //    - Dynamic Page Cache will cache a different result for when the
    +    //      request is unauthenticated (this 401) versus authenticated (some
    +    //      other response)
    +    // 2. have the 'config:user.role.anonymous' cache tag, because the only
    +    //    reason this 401 would no longer be a 401 is if permissions for the
    +    //    'anonymous' role change, causing that cache tag to be invalidated.
    +    // @see \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
    +    // @see \Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber()
    +    // @see \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onAllResponds()
    +    $cacheability = CacheableMetadata::createFromObject($site_config)
    +      ->addCacheTags(['config:user.role.anonymous'])
    +      ->addCacheContexts(['user.roles:anonymous']);
    +    return new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous);
    

    This change actually needs to land independently anyway probably.

    So then this can serve as the first use case, along with landing the necessary infrastructure (the various Cacheable*Exception classes). After that issue/patch lands, we can continue here, and the patch will be significantly smaller!

If you want to help this issue land, then also see you in #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata, where we will make it much easier to land this patch!

Wim Leers’s picture

Title: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage » [PP-1] Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage
Issue summary: View changes
Status: Needs review » Postponed

#2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata now has a reviewable (and hopefully RTBC'able) green patch. It's 31.8 KB. It'd cut the size of the patch in this issue down to about 40 KB, which obviously would make it much more focused and reviewable.

So I'm just going to postpone this issue on #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata.

Wim Leers’s picture

#2874938: AdminRouteSubscriber must only mark HTML routes as administrative has been committed, which means we can (and must) now remove much of the complexity in this patch:

+++ b/core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php
@@ -31,8 +33,21 @@ protected function getAuthenticationRequestOptions($method) {
+    // @todo Entity resources with URLs that begin with '/admin/' are marked as

+++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
@@ -91,11 +91,41 @@ protected function getAuthenticationRequestOptions($method) {
+      // @todo Entity resources with URLs that begin with '/admin/' are marked
...
+      if (strpos($this->entity->getEntityType()->getLinkTemplate('canonical'), '/admin/') !== 0) {

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -373,7 +388,13 @@ public function testGet() {
+    if (strpos($this->entity->getEntityType()->getLinkTemplate('canonical'), '/admin/') !== 0) {

@@ -515,7 +523,17 @@ public function testGet() {
+      // @todo Entity resources with URLs that begin with '/admin/' are marked
...
+      if (strpos($this->entity->getEntityType()->getLinkTemplate('canonical'), '/admin/') !== 0) {

@@ -549,7 +567,17 @@ public function testGet() {
+      // @todo Entity resources with URLs that begin with '/admin/' are marked
...
+      if (strpos($this->entity->getEntityType()->getLinkTemplate('canonical'), '/admin/') !== 0) {

@@ -585,7 +613,31 @@ public function testGet() {
+    // @todo Entity resources with URLs that begin with '/admin/' are marked as
...
+    if (strpos($this->entity->getEntityType()->getLinkTemplate('canonical'), '/admin/') !== 0) {

@@ -602,7 +654,17 @@ public function testGet() {
+    // @todo Entity resources with URLs that begin with '/admin/' are marked as
...
+    if (strpos($this->entity->getEntityType()->getLinkTemplate('canonical'), '/admin/') !== 0) {

All of this can be removed!

(That was not a blocker, this issue is still blocked on #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata, but it does help make this patch much more understandable! 😀)

From 189 lines changed in EntityResourceTestBase to only 140! (And (Basic|Cookie)ResourceTestTrait also each lost ~10 lines of changes.)

Wim Leers’s picture

Title: [PP-1] Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage » Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage
Status: Postponed » Needs review
FileSize
45.58 KB

Aaaaand #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata is in!

Straight rebase.

@borisson_, the honor to RTBC is yours :D

borisson_’s picture

Status: Needs review » Reviewed & tested by the community

Read trough the patch again - just to make sure the rebase worked and I couldn't find anything to change. Looks solid!

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 232: rest_cacheable_response-2765959-232.patch, failed testing. View results

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
46.44 KB

Automatic rebase courtesy of git.

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/rest/src/RequestHandler.php
@@ -133,7 +133,10 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
     if ($response instanceof CacheableResponseInterface) {
-      // Add rest config's cache tags.
+      // Add global rest settings config's cache tag, for BC flags.
+      // @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
+      // @see \Drupal\rest\EventSubscriber\RestConfigSubscriber
+      $response->getCacheableMetadata()->addCacheTags(['config:rest.settings']);

This should load the config object and use Config::getCacheTags(). If there's a reason to not do that, should add a comment why. Everything else looks good.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

👌 great nit! On it.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Needs work » Reviewed & tested by the community
FileSize
2.64 KB
48.02 KB
borisson_’s picture

Status: Reviewed & tested by the community » Needs work

Sorry Wim, one more nit.

+++ b/core/modules/rest/src/RequestHandler.php
@@ -34,20 +35,33 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
+   * The config factory.
...
+   *   The configuration factory.

Why is it config one time, and configuration the other? The rest of core seems to be using config factory more often, let's use that.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
835 bytes
48.02 KB

Both were copy/pasted from \Drupal\Core\EventSubscriber\FinalExceptionSubscriber, but happy to fix!

borisson_’s picture

Awesome, thanks! RTBC++

The last submitted patch, 238: rest_cacheable_response-2765959-238.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 240: rest_cacheable_response-2765959-240.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
1.29 KB
49.25 KB

RequestHandlerTest failed, because I forgot to update it to inject the config factory service. Wim--

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 244: rest_cacheable_response-2765959-244.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
2.42 KB
51.59 KB

#244 suddenly fails because #2800873: Add XML GET REST test coverage, work around XML encoder quirks landed. That added ConfigurableLanguageXmlBasicAuthTest and ContentLanguageSettingsXmlBasicAuthTest (among many other tests), and those two tests are now failing. Same problem as #217. Trivial fix :)

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Ok, Wim stepped me through this patch for about an hour. While it's big in size, the actual changes are fairly small, and mostly confined to updating tests.

I had the same question as @borisson_ about whether or not we had benchmarks for this, but Wim explained (as he did in #226) that this is merely applying the same pattern as #2827797: ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster so there's no sense in re-hashing this here.

Also nice that fixing this problem uncovered a couple of other subtle bugs with places where we were missing cacheability metadata. Rock! :D

I see @catch's concerns have been addressed, sooooo...

Committed and pushed to 8.5.x. Thanks!

  • webchick committed dd63977 on 8.5.x
    Issue #2765959 by davidwbarratt, Wim Leers, dawehner, borisson_,...
webchick’s picture

Pushed, though I had to remove a missing use statement and undo some permission changes before it let me. :)

FILE: ...serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
----------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
----------------------------------------------------------------------
 8 | WARNING | [x] Unused use statement
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
...
git pre-commit check failed: file core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php should be 644 not 755
git pre-commit check failed: file core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php should be 644 not 755
git pre-commit check failed: file core/modules/rest/tests/src/Functional/EntityResource/Block/BlockResourceTestBase.php should be 644 not 755
Wim Leers’s picture

Yay!

#249: sorry about that :(

I followed through on all the things:

  1. CR (https://www.drupal.org/node/2920529) was already live, but improved it.
  2. Posted #2905563-34: REST: top priorities for Drupal 8.5.x to share the good news!
  3. Posted #2867881-4: Remove @todo and workaround in Cookie ResourceTestTrait plus a patch: that issue now has clear next steps. (Bug in block module discovered in this issue.)
  4. Posted an updated patch at #2893804-11: Remove rest.module BC layers: more code that can be removed when BC layers are removed in 9.0.0!
Wim Leers’s picture

Also: was finally able to delete these branches:

  rest_cacheable_response-2765959
  rest_cacheable_response-2765959-FINAL
  rest_cacheable_response-2765959-FINAL-june
  rest_cacheable_response-2765959_WOOT_WORKING_AFTER_LOTS_OF_SWEAT

😅

Wim Leers’s picture

Wim Leers’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Wim Leers’s picture

Issue tags: +8.5.0 highlights

This so vastly improves the performance for certain REST API client use cases that I think it's worth a highlight.

davidwbarratt’s picture

+1 to that!