Problem/Motivation

Quoting #2737751: Refactor REST routing to address its brittleness (the parent issue), and only the bullets that are closely related and that this issue wants to solve:

It's very hard to understand routing in the REST module, and it's very brittle, because:

  1. ResourceBase only sets _format for GET route, should also do so for POST/PATCH/DELETE routes.
  2. ResourceBase generates one route per method for POST/PATCH/DELETE, but one route per method+format for GET. This should be made consistent.
  3. ResourceBase generates individual GET routes for every available serialization format, not just the ones that are allowed by the configuration, and then ResourceRoutes::alter() removes those that are not acceptable by the configuration.
  4. ResourceBase sets _content_type_format for PATCH/POST, but it should actually also set _format.

  1. ResourceRoutes::alter() is doing configuration verification.
  2. ResourceRoutes::alter() assumes _format has only ever one format listed, even though multiple can be listed. This blows up as soon as any custom @RestResource plugin specifies multiple formats, which it can by not using ResourceBase::routes().

Proposed resolution

  1. Add _format route requirement to POST and PATCH routes.
  2. Have a single route for GET, instead of one per format.
  3. Fix the _format requirement validation in ResourceRoutes so that it supports multiple formats (also necessary for point 2).
  4. Per #9, this sadly requires significant bugfixes for \Drupal\Core\Routing\LazyRouteFilter and related code, because it's not respecting route filter service priorities. Fixed in #2472337: Provide a lazy alternative to service collectors which just detects service IDs and #2883680: Force all route filters and route enhancers to be non-lazy, which were both blocking this issue.
  5. Still per #9: the route filter priorities are incorrect, too! Quoting the relevant portion:

    because nothing in the entirety of Drupal 8: not the REST module, not anything else in core, not even the routing system test coverage… had both a _format and a _content_type_format route requirement. And hence… we didn't even notice that even the existing route_filter service priorities were incorrect. method_filter had priority 1, all others (request_format_route_filter and content_type_header_matcher) had no priority, i.e. priority 0.
    But content_type_header_matcher must run before request_format_route_filter! Because when a route has a _content_type_format restriction, that means it accepts/needs a request body. Without the request body, no sensible response. Missing a Content-Type request header causes a 415. Missing a ?_format (or Accept request header in an ideal world) causes a 406.

    The tests are failing because a 415 is expected, but a 406 is received. In other words: exactly the problem I just explained.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
#146 2858482-146.patch41.8 KBwim leers
#146 interdiff-137-146.txt4.14 KBwim leers
#146 interdiff-129-146.txt2.69 KBwim leers
#142 2858482-142.patch41.03 KBwim leers
#142 interdiff-137-142.txt1.45 KBwim leers
#142 interdiff-129-142.txt5.34 KBwim leers
#137 2858482-137.patch42.05 KBwim leers
#137 interdiff-129-137.txt6.47 KBwim leers
#129 2858482-129.patch41.88 KBwim leers
#129 interdiff-124-129.txt48.35 KBwim leers
#124 2858482-124.patch41.75 KBwim leers
#120 2858482-120.patch41.76 KBwim leers
#120 interdiff.txt450 byteswim leers
#114 2858482-114.patch41.78 KBwim leers
#114 interdiff.txt2.25 KBwim leers
#110 2858482-110.patch40.53 KBwim leers
#110 interdiff.txt878 byteswim leers
#104 2858482-104.patch40.51 KBwim leers
#104 interdiff.txt1.87 KBwim leers
#103 2858482-103.patch40.11 KBwim leers
#103 interdiff.txt1.71 KBwim leers
#102 2858482-102.patch39.26 KBwim leers
#96 interdiff-2858482.txt1.23 KBdawehner
#96 2858482-96.patch40 KBdawehner
#92 2858482-92.patch40.57 KBwim leers
#92 interdiff.txt24.77 KBwim leers
#90 2858482-90.patch15.85 KBwim leers
#90 interdiff.txt19.64 KBwim leers
#85 2858482-84.patch32.21 KBwim leers
#85 interdiff.txt405 byteswim leers
#82 2858482-82.patch31.82 KBwim leers
#82 interdiff.txt2.32 KBwim leers
#81 2858482-81.patch32.42 KBwim leers
#79 2858482-79.patch32.47 KBwim leers
#79 interdiff.txt405 byteswim leers
#76 2858482-76.patch32.81 KBwim leers
#74 2858482-74.patch32.86 KBwim leers
#74 interdiff.txt3.78 KBwim leers
#69 2858482-67.patch31.71 KBwim leers
#67 interdiff-2858482.txt344 bytesdawehner
#67 2858482-67.patch31.86 KBdawehner
#65 2858482-65.patch31.38 KBwim leers
#65 interdiff.txt10.4 KBwim leers
#63 2858482-63.patch26.42 KBwim leers
#63 interdiff.txt15.23 KBwim leers
#62 2858482-62.patch15.49 KBwim leers
#62 interdiff.txt3.68 KBwim leers
#59 2858482-55-proper.patch15.91 KBwim leers
#55 2858482-55.patch16.59 KBwim leers
#55 interdiff.txt6.37 KBwim leers
#48 2858482-48.patch11.8 KBwim leers
#48 interdiff.txt2.35 KBwim leers
#46 2858482-46.patch10.89 KBwim leers
#46 interdiff.txt2.79 KBwim leers
#43 2858482-43.patch10.76 KBwim leers
#43 interdiff.txt1.35 KBwim leers
#41 2858482-41.patch9.44 KBwim leers
#38 2858482-38.patch19.45 KBwim leers
#38 interdiff.txt651 byteswim leers
#33 2858482-33.patch20.03 KBwim leers
#33 interdiff.txt8.3 KBwim leers
#32 2858482-32.patch17.66 KBwim leers
#32 interdiff.txt4.33 KBwim leers
#31 2858482-31.patch17.18 KBwim leers
#29 2858482-29.patch17.22 KBwim leers
#22 2858482-22.patch17.29 KBwim leers
#22 interdiff.txt3.8 KBwim leers
#20 2858482-20.patch16.55 KBwim leers
#16 2858482-16.patch16.6 KBwim leers
#16 interdiff.txt6.54 KBwim leers
#14 2858482-14.patch17.08 KBwim leers
#14 interdiff.txt2.49 KBwim leers
#10 2858482-10.patch14.65 KBwim leers
#10 interdiff.txt651 byteswim leers
#9 2858482-9.patch14.06 KBwim leers
#9 interdiff.txt6.58 KBwim leers
#7 2858482-6.patch7.57 KBwim leers
#7 interdiff.txt2.12 KBwim leers
#3 2858482-3.patch5.5 KBwim leers
#3 interdiff.txt731 byteswim leers
#2 2858482-2.patch5.25 KBwim leers

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

Assigned: Unassigned » wim leers
Status: Active » Needs review
StatusFileSize
new5.25 KB

This is basically all that's needed in this issue.

wim leers’s picture

StatusFileSize
new731 bytes
new5.5 KB

Let's be more clear in ResourceBase on why DELETE doesn't need _format nor _content_type_format route requirements. Explicit documentation helps prevent questions later.

The last submitted patch, 2: 2858482-2.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 3: 2858482-3.patch, failed testing.

dawehner’s picture

+++ b/core/modules/rest/src/Plugin/ResourceBase.php
@@ -126,16 +128,13 @@ public function routes() {
-          foreach ($this->serializerFormats as $format_name) {
...
-            $collection->add("$route_name.$method.$format_name", $format_route);

I''m a bit worried about custom code linking to these routes, as this would throw exceptions now.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new2.12 KB
new7.57 KB

I didn't notice that I also needed to update DbLogResourceTest. Easy fix. That fixes one of the failures.

Status: Needs review » Needs work

The last submitted patch, 7: 2858482-6.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
Issue tags: +WSCCI
Related issues: +#2368769: All route enhancers are run on every request
StatusFileSize
new6.58 KB
new14.06 KB

All remaining failures are the same. They're all about testPost() failing at EntityResourceTestBase.php:633:

Failed asserting that 406 is identical to 415.

(And because #2832859: Write EntityResourceTestBase subclasses for: MenuLinkContent just got committed, I think the number of failures will actually be higher in #7 than in #3, despite fixing one failure).

Why is this happening?

  1. Because the POST route only had _content_type_format before, now it has _format.
  2. This causes \Drupal\Core\Routing\RequestFormatRouteFilter::applies() to return TRUE instead of FALSE.
  3. The compiled route has a route option called _route_filters. It used to be ['method_filter', 'content_type_header_matcher'] before this patch. But point 2 causes it to be ['request_format_route_filter', 'method_filter', 'content_type_header_matcher'] after this patch.
  4. And then we get to the actual bug: #2368769: All route enhancers are run on every request introduced \Drupal\Core\Routing\LazyRouteFilter and effectively made every route filter a lazy one. But… it did so without respecting the priority of route_filter-tagged services! Why was this not noticed? Because REST had almost no test coverage. And neither does the routing system for WSCCI functionality, i.e. REST functionality. And so the handful of route filters that we have just ran in a random order (well, somewhat deterministic, but definitely not respecting the service priority).

On top of that, because nothing in the entirety of Drupal 8: not the REST module, not anything else in core, not even the routing system test coverage… had both a _format and a _content_type_format route requirement. And hence… we didn't even notice that even the existing route_filter service priorities were incorrect. method_filter had priority 1, all others (request_format_route_filter and content_type_header_matcher) had no priority, i.e. priority 0.
But content_type_header_matcher must run before request_format_route_filter! Because when a route has a _content_type_format restriction, that means it accepts/needs a request body. Without the request body, no sensible response. Missing a Content-Type request header causes a 415. Missing a ?_format (or Accept request header in an ideal world) causes a 406.

The tests are failing because a 415 is expected, but a 406 is received. In other words: exactly the problem I just explained.


So this patch then:

  1. causes LazyRouteFilter to respect the priorities for route_filter-tagged services. It looks at \Drupal\Core\Routing\Router for guidance — it uses the exact same approach.
  2. also fixes the priority of the content_type_header_matcher: it must run before the request_format_route_filter

Yes, this arguably belongs in a separate issue. We can debate that later. I first want to get this to green. To prove that this solves the problems uncovered here.

wim leers’s picture

StatusFileSize
new651 bytes
new14.65 KB

#2368769: All route enhancers are run on every request also didn't make ContentTypeHeaderMatcher actually lazy. Let's do that.

The last submitted patch, 9: 2858482-9.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 10: 2858482-10.patch, failed testing.

dawehner’s picture

I believe #2472337: Provide a lazy alternative to service collectors which just detects service IDs would help us a lot.

On the longrun I agree. We should make every route filter/enhancer lazy or simply initiatlive it always. That for itself gets rid of 2 indirections which aren't needed.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new2.49 KB
new17.08 KB

This fixes all fails.

In \Drupal\rest\Tests\ResourceTest::testSerializationClassIsOptional() and \Drupal\user\Tests\RestRegisterUserTest::registerRequest(), the ?_format= query string was missing for POST requests.

And \Drupal\rest\Tests\Update\ResourceGranularityUpdateTest failed because \Drupal\rest\Routing\ResourceRoutes::getRoutesForResourceConfig() now is updating _format correctly for the first time. In doing so, it is omitting the hal_json format… because the hal module was not actually enabled in this test! So the fix was simple, change:

$extensions = unserialize($extensions);
$extensions['module']['rest'] = 0;
$extensions['module']['serialization'] = 0;
$connection->update('config')
  ->fields([
    'data' => serialize($extensions),
  ])

to:

$extensions = unserialize($extensions);
$extensions['module']['hal'] = 0;
$extensions['module']['rest'] = 0;
$extensions['module']['serialization'] = 0;
$connection->update('config')
  ->fields([
    'data' => serialize($extensions),
  ])
dawehner’s picture

+++ b/core/core.services.yml
+++ b/core/core.services.yml
@@ -824,6 +824,7 @@ services:

@@ -824,6 +824,7 @@ services:
     class: Drupal\Core\Routing\LazyRouteFilter
     tags:
       - { name: non_lazy_route_filter }
+      - { name: service_collector, tag: route_filter, call: addLazyRouteFilter }

The problem is, by adding this tag, this makes it not lazy anymore, see the issue linked above.

wim leers’s picture

StatusFileSize
new6.54 KB
new16.6 KB

Aha. I didn't realize that when I first read your comment. Sorry! Thanks for taking the time to point it out so clearly :)

We can implement it without using service_collector in this patch, to avoid making it non-lazy. Did that in this reroll.

wim leers’s picture

Issue summary: View changes
wim leers’s picture

Status: Needs review » Needs work

The last submitted patch, 16: 2858482-16.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new16.55 KB

I forgot --binary. This is the patch that the one in #16 should have been.

tedbow’s picture

Status: Needs review » Needs work

Looking good! The multiple routes for each format on GET confused me before.

  1. +++ b/core/core.services.yml
    @@ -940,11 +940,11 @@ services:
    +      - { name: route_filter, priority: 10 }
    ...
    +      - { name: route_filter, priority: 5 }
    

    We should document why these priorities need to be set to these values so we remember and don't accidentally mess this up.

  2. +++ b/core/modules/rest/src/Plugin/ResourceBase.php
    @@ -126,16 +128,13 @@ public function routes() {
    +          $collection->add("$route_name.$method", $route);
    ...
               $collection->add("$route_name.$method", $route);
    

    Right now $collection->add("$route_name.$method", $route); is in every case of this switch block. I think it was needed to be there before because 'GET', 'HEAD' case added multiple routes. Since this patches removes that I think would it be in scope to move adding the route to after the switch block. It would be more readable I think because the switch block would only concern modifications needed, if any, before adding the route to the collection

  3. +++ b/core/modules/rest/src/Routing/ResourceRoutes.php
    @@ -106,11 +106,20 @@ protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_
    +          $allowed_formats = array_intersect($format_requirements, $rest_resource_config->getFormats($method));
    +          $route->setRequirement('_format', implode('|', $allowed_formats));
    ...
    +            continue;
    +          }
    

    Why do we check for empty($allowed_formats) after we call setRequirement() with $allowed_formates? If $allowed_formats is empty we are not going to add it to the collection so why do that check before the call to setRequirements()?

It should be noted that after this patch the ids of routes will change. For example you would no longer have "rest.entity.node.GET.hal_json" and "rest.entity.node.GET.json" it would just be "rest.entity.node.GET". I checked https://www.drupal.org/core/d8-bc-policy and route names are note consider API but think something we would want to note in a change record incase any custom code is checking \Drupal\Core\Routing\RouteMatchInterface::getRouteName. If search core for "->getRouteName() == " you will see it a bunch not related to REST though.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new3.8 KB
new17.29 KB
  1. Agreed! Done. (This was done in #9, detailed explanation there.)
  2. Done.
  3. Hah, nice simplification, done :)

RE: REST GET routes going from one-per-format-per-resource (N) to one-per-resource (1): change record created: https://www.drupal.org/node/2865645.

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

@Wim Leers thanks for the update. Changes look good!

Marking as RTBC. I also updatded the change record with a couple sentences of explanation.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterLazyRouteFilters.php
    @@ -21,7 +21,8 @@ public function process(ContainerBuilder $container) {
     
         foreach ($container->findTaggedServiceIds('route_filter') as $id => $attributes) {
    -      $service_ids[$id] = $id;
    +      $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
    +      $service_ids[$priority][] = $id;
         }
    

    It is sad to see that we don't solve the generic solution out there #2472337: Provide a lazy alternative to service collectors which just detects service IDs ... given how near we are to solving that. It feels like a local time minimum, which doesn't sum up to be a minimum in total.

  2. +++ b/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php
    @@ -54,7 +54,7 @@ public function filter(RouteCollection $collection, Request $request) {
       public function applies(Route $route) {
    -    return TRUE;
    +    return $route->hasRequirement('_content_type_format');
       }
     
    

    I'm wondering what this means in the context of this code:

        foreach ($collection as $name => $route) {
          $supported_formats = array_filter(explode('|', $route->getRequirement('_content_type_format')));
          if (empty($supported_formats)) {
            // No restriction on the route, so we move the route to the end of the
            // collection by re-adding it. That way generic routes sink down in the
            // list and exact matching routes stay on top.
            $collection->add($name, $route);
          }

    ... doesn't this mean that this entire if (empty()) is then dead code?

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 22: 2858482-22.patch, failed testing.

wim leers’s picture

Title: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent » [PP-1] Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
Status: Needs work » Postponed

#24:

  1. Okay, let's postpone this on #2472337: Provide a lazy alternative to service collectors which just detects service IDs then. (I didn't mean to annoy/offend, I just wanted to see some progress. It takes forever to get anything REST-related committed, which is why 1/3rd of the core RTBC queue is currently REST-related…)
  2. Excellent point. Let's fix that when this is unblocked again.
wim leers’s picture

Updated #2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle() to indicate that it's now blocked on a chain of two issues. And updated #2737751: Refactor REST routing to address its brittleness to indicate it's now blocked on a chain of 3 issues.

wim leers’s picture

Title: [PP-1] Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent » Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
Status: Postponed » Needs work
wim leers’s picture

Status: Needs work » Needs review
Related issues: +#2826407: PATCH + POST allowed format validation happens in RequestHandler::handle(), rather than in the routing system
StatusFileSize
new17.22 KB

This conflicted with #2826407: PATCH + POST allowed format validation happens in RequestHandler::handle(), rather than in the routing system.

  1. This reroll only resolves the conflict; it does not address #24.
  2. I also still have to look into potential changes to the approach in this patch, because REST has improved quite a bit since March (2 months ago!), which is when this patch was last updated.

Status: Needs review » Needs work

The last submitted patch, 29: 2858482-29.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new17.18 KB
error: cannot apply binary patch to 'core/modules/rest/tests/fixtures/update/drupal-8.rest-rest_post_update_resource_granularity.php' without full index line

git is a git with complex files.

Rerolled with --binary, without any other changes.

wim leers’s picture

StatusFileSize
new4.33 KB
new17.66 KB

Addressed #29.2: I saw ways to further clean this up.

#2826407: PATCH + POST allowed format validation happens in RequestHandler::handle(), rather than in the routing system moved the setting of the _content_type_format route requirement from ResourceBase::routes() to ResourceRoutes::getRoutesForResourceConfig. But this patch was adding the setting of the _format route requirement to ResourceBase::routes(). Ideally both would be set in the same place, right? Because both suffer from the same problem: we can't set them in ResourceBase::routes() (or ResourceInterface::routes() in general) because REST resource plugins do not have access to the REST resource configuration…

So if we accept that premise, and we then choose to remove the setting of _format from ResourceBase::routes(), then there's nothing left to do for the PATCH, GET, HEAD and DELETE methods. The only logic that remains, is that for POST, to specify a different path than the canonical one.

Which means we can delete that entire confusing switch statement, and make it super simple. Then it becomes very clear that ResourceBase::routes() really actually is returning base routes by default: the _format and _content_type_format have to be specified later, when we can read the actual configured formats for this resource from its config entity!

This also means that the changes this patch was making to ResourceRoutes:::getRoutesforResourceConfig() actually can
be simplified to match that of #2826407: PATCH + POST allowed format validation happens in RequestHandler::handle(), rather than in the routing system. And we can even slightly simplify the logic that #2826407 added there, and make both the _content_type_format and the _format cases consistent.

wim leers’s picture

Issue summary: View changes
StatusFileSize
new8.3 KB
new20.03 KB

Addressed #24.1, by using the new functionality introduced by #2472337: Provide a lazy alternative to service collectors which just detects service IDs. I followed the best practices/core committer requirements that were established in that issue, which includes not using ContainerAware. I also tried to minimize changes in LazyRouteFilter, but felt obligated to correct the most misleading pieces of documentation present in LazyRouteFilter.

Note: it's probably easier to not interview the interdiff, but review the overall patch, and look specifically at the LazyRouteFilter-related changes.

IS updated, CR (https://www.drupal.org/node/2598944) updated.

wim leers’s picture

#24.2: you're right. But in order to achieve that, we'd have to ensure that \Drupal\Core\Routing\LazyRouteFilter::filter() actually respected the the route filters specified for each specific route. And that's not how \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface (filter()) was designed: it checks every route in the collection, regardless. Drupal layered \Drupal\Core\Routing\RouteFilterInterface (applies()) on top of that, solely for the route compilation phase.

i.e. applies() determines which filters need to be applied to a collection of initial route matches (based on URL) to filter() the route collection down to a smaller number (based on method, format, etc).

What you're saying means that every route filter implementation must ensure that every filter() implementation must also call applies() for every route it iterates over in its collection. Which means requiring pure Symfony API implementations (just filter()) to be aware of the Drupal API addition (applies()) and conditionally change their logic.

So I think it's safer to keep this as it is today. We could open a separate issue for that though.

The change you quoted is still valid: it ensures that we only run the ContentTypeHeaderMatcher route filter when there's actually at least one route in the route collection that needs it!

wim leers’s picture

Assigned: wim leers » Unassigned

Whew, now blocked on review!

wim leers’s picture

Issue tags: +API-First Initiative
dawehner’s picture

#24.2: you're right. But in order to achieve that, we'd have to ensure that \Drupal\Core\Routing\LazyRouteFilter::filter() actually respected the the route filters specified for each specific route. And that's not how \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface (filter()) was designed: it checks every route in the collection, regardless. Drupal layered \Drupal\Core\Routing\RouteFilterInterface (applies()) on top of that, solely for the route compilation phase.

I just proposed a change to make route filters and enhancers much better for themselves: #2883680: Force all route filters and route enhancers to be non-lazy I'm wondering what you think about it.

The change you quoted is still valid: it ensures that we only run the ContentTypeHeaderMatcher route filter when there's actually at least one route in the route collection that needs it!

This would be indeed possible, but it feels a bit like an overoptimisation.

wim leers’s picture

StatusFileSize
new651 bytes
new19.45 KB

I just proposed a change to make route filters and enhancers much better for themselves

Commented there. +1 :) But I don't think you are saying that issue should block this one, right?

This would be indeed possible, but it feels a bit like an overoptimisation.

Fine, reverted.

(I'm confused by this though: ContentTypeHeaderMatcher is only very rarely necessary, yet HEAD (and this revert) causes it to run always. So then what's the point of it all? Of course, if I understand #2883680: Force all route filters and route enhancers to be non-lazy correctly, it'll remove this altogether, so then making this change now indeed is pointless.)

wim leers’s picture

Title: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent » [PP-1] Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
Status: Needs review » Postponed
Related issues: +#2883680: Force all route filters and route enhancers to be non-lazy

This was first blocked on #2472337: Provide a lazy alternative to service collectors which just detects service IDs for almost 3 months. The day after that landed, I got this patch going again (#28 through #36).

But now it's being suggested in #37 that this gets blocked on #2883680: Force all route filters and route enhancers to be non-lazy. I think #2883680 is a worthwhile simplification. But despite me having worked on that for half a day today to get it to green, it's still not green. And for it to get to RTBC, performance (profiling) will have to show positive results (i.e. faster or same speed).

I'm fine with delaying this on #2883680: Force all route filters and route enhancers to be non-lazy for a few weeks, but I don't want this to be blocked again for multiple months. That'd mean 3 REST issues are again blocked for months (this issue, plus #2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle() which is blocked on this, plus #2737751: Refactor REST routing to address its brittleness which is blocked on that). If this were introducing nasty work-arounds, then I'd agree with you that we should hard-block this. on #2883680. But this issue is merely fixing the existing infrastructure, #2883680 is completely refactoring the existing infrastructure.

Postponing for now.

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

Title: [PP-1] Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent » Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
Status: Postponed » Needs review
StatusFileSize
new9.44 KB

#2883680: Force all route filters and route enhancers to be non-lazy (finally — after almost 4 months…) landed, which means this is now fully unblocked!

  1. index 796ece3..a59190c 100644
    --- a/core/core.services.yml
    
    index 8a22bc5..719df12 100644
    --- a/core/lib/Drupal/Core/CoreServiceProvider.php
    
    index d774779..0000000
    --- a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterLazyRouteFilters.php
    
    index 356fe3e..5299604 100644
    --- a/core/lib/Drupal/Core/Routing/LazyRouteEnhancer.php
    
    index 10f3d51..f6672ff 100644
    --- a/core/lib/Drupal/Core/Routing/LazyRouteFilter.php
    

    All these changes are obsolete now that #2883680: Force all route filters and route enhancers to be non-lazy is in! :)

  2. --- a/core/modules/rest/src/Tests/ResourceTest.php
    +++ b/core/modules/rest/src/Tests/ResourceTest.php
    

    The changes here are obsolete because #2775553: Convert web tests to browser tests for REST module refactored this test a few months ago.

So many obsolete changes, we can drop all their hunks, which means this patch just became a lot smaller!

Status: Needs review » Needs work

The last submitted patch, 41: 2858482-41.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.35 KB
new10.76 KB

I omitted too many hunks in #41:

+++ b/core/core.services.yml
@@ -938,15 +939,21 @@ services:
   request_format_route_filter:
     class: Drupal\Core\Routing\RequestFormatRouteFilter
     tags:
+      # The request format route filter must run last.
       - { name: route_filter }
   method_filter:
     class: Drupal\Core\Routing\MethodFilter
     tags:
-      - { name: route_filter, priority: 1 }
+      # The HTTP method route filter must run first: based on the request method, content type request header-based
+      # route filtering (content_type_header_matcher) may or may not be necessary.
+      - { name: route_filter, priority: 10 }
   content_type_header_matcher:
     class: Drupal\Core\Routing\ContentTypeHeaderMatcher
     tags:
-      - { name: route_filter }
+      # The content type request header router filter must run before the request format route filter
+      # (request_format_route_filter), because without a valid request body (for HTTP methods that need it, such as POST
+      # and PATCH), no successful response is possible.
+      - { name: route_filter, priority: 5 }

These fixes the the current priorities (plus comments to explain/justify them) are still necessary.

wim leers’s picture

Issue summary: View changes

IS updated. IS now also explains why #43 is making the patch pass.

dawehner’s picture

  1. +++ b/core/core.services.yml
    @@ -932,15 +932,21 @@ services:
    +      # The request format route filter must run last.
    ...
    +      # The HTTP method route filter must run first: based on the request method, content type request header-based
    +      # route filtering (content_type_header_matcher) may or may not be necessary.
    

    🔧 It would be nice to have some explanation why this particular order of priorities are chosen. At least I was not able to understand why from these comments :(

  2. +++ b/core/core.services.yml
    @@ -932,15 +932,21 @@ services:
    +      # The content type request header router filter must run before the request format route filter
    

    ❓Do we have the 80 chars rule in comments in yml files as well?

  3. +++ b/core/modules/rest/src/Plugin/ResourceBase.php
    @@ -114,30 +114,16 @@ public function routes() {
    -        case 'GET':
    -        case 'HEAD':
    -          // Restrict GET and HEAD requests to the media type specified in the
    -          // HTTP Accept headers.
    -          foreach ($this->serializerFormats as $format_name) {
    -            // Expose one route per available format.
    -            $format_route = clone $route;
    -            $format_route->addRequirements(['_format' => $format_name]);
    -            $collection->add("$route_name.$method.$format_name", $format_route);
    -          }
    

    ❓I'm wondering whether we need to keep those around for BC reasons (someone might have overridden this base class or link to those routes using route names). This would be a massive problem of course

  4. +++ b/core/modules/rest/src/Plugin/ResourceBase.php
    @@ -114,30 +114,16 @@ public function routes() {
    +      $path = $method !== 'POST'
    +        ? $canonical_path
    +        : $create_path;
    +      $route = $this->getBaseRoute($path, $method);
    

    I think its always easier to read positive conditions so $method === 'POST' ? $create_path : $canonical_path;

wim leers’s picture

StatusFileSize
new2.79 KB
new10.89 KB

Thanks for the review! 👏

  1. ✅ Done.
  2. ✅ AFAICT yes. Thanks, fixed!
  3. ❓ Keeping them around for BC would completely defeat the purpose of this issue, which is to make REST routing actually maintainable. I think the number of people overriding/altering REST routes can be counted on two hands at most, and the change in code that this patch would require them to make would make their code actually simpler.
  4. ✅ Okay, done. (My rationale for doing it this way is that the === POST is the less common case. I always put the common case first. I don't feel have a strong opinion about this though.)
dawehner’s picture

❓ Keeping them around for BC would completely defeat the purpose of this issue, which is to make REST routing actually maintainable. I think the number of people overriding/altering REST routes can be counted on two hands at most, and the change in code that this patch would require them to make would make their code actually simpler.

I agree with you, but what about people using the route names to link to things? We could probably provide some routes which aren't involved in actual routing, simply for BC reasons? Maybe its not worth doing so though.

wim leers’s picture

StatusFileSize
new2.35 KB
new11.8 KB

That's fair. If at all possible, we shouldn't break BC. And it's possible in this case. Although doing this does mean

We could probably provide some routes which aren't involved in actual routing, simply for BC reasons?

Symfony's routing system has no concept of "route aliases", so we can't do this. There are only two alternatives:

  1. use Symfony subrequests
  2. provide duplicate route definitions

The latter is the simplest approach, and is also the strategy we used last time we moved routes around (in #2753681: Move CSRF header token out of REST module so that user module can use it, as well as any contrib module).

dawehner’s picture

Symfony's routing system has no concept of "route aliases", so we can't do this. There are only two alternatives:

Well, this is not entirely true. Just think about <front> and <current>.

wim leers’s picture

<front> and <current> don't use the (Symfony) routing system. They use Drupal's special sauce on top: path/route processors. \Drupal\Core\PathProcessor\PathProcessorFront + \Drupal\Core\RouteProcessor\RouteProcessorCurrent.

dawehner’s picture

@Wim Leers
Right, well, this is what I'm saying. We could provide a BC way which doesn't introduce having actual routes in the router.

wim leers’s picture

But <front> and <current> also exist in the router. They just have very thin route definitions, because the bulk of the logic lives in path/route processors. Why is that better/preferable to what #48 does?

dawehner’s picture

+++ b/core/modules/rest/src/Plugin/ResourceBase.php
@@ -124,6 +124,17 @@ public function routes() {
+      // BC: the REST module originally created per-format GET routes, instead
+      // of a single route (likely as a work-around for broken '_format' route
+      // requirement validation in ResourceRoutes).
+      if ($method === 'GET' || $method === 'HEAD') {
+        foreach ($this->serializerFormats as $format_name) {
+          $format_route = clone $route;
+          $format_route->addRequirements(['_format' => $format_name]);
+          $collection->add("$route_name.$method.$format_name", $format_route);
+        }
+      }

I'm simply fearing by adding them into the routing process as well, these routes might be for example used by the router itself.
I just tried to suggest a BC layer with the most minimal surface.

wim leers’s picture

Assigned: Unassigned » wim leers
Status: Needs review » Needs work

#53 is an excellent point. I hadn't looked at it that way yet. I'll try to do that instead.

wim leers’s picture

Assigned: wim leers » Unassigned
Status: Needs work » Needs review
StatusFileSize
new6.37 KB
new16.59 KB

I'm glad @dawehner made that remark and insisted on it — \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface is indeed the perfect fit! Thanks, Daniel!

It seems to work nicely. It is a bit more code, but indeed helps ensure it's a BC layer with the smallest surface possible.

Status: Needs review » Needs work

The last submitted patch, 55: 2858482-55.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review

Testbot is working again. Retesting, back to NR.

Status: Needs review » Needs work

The last submitted patch, 55: 2858482-55.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new15.91 KB

Sigh, forgot git diff --binary.

borisson_’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/rest/src/Plugin/ResourceBase.php
    @@ -114,29 +114,26 @@ public function routes() {
    +      // @todo _format and _content_type_format route requirements are added in
    +      //       ResourceRoutes::getRoutesForResourceConfig(), because their
    +      //       values depend on the RestResourceConfig entity, which
    +      //       @RestResource plugins cannot access. Fix this in Drupal 9.
    

    While we don't have a coding standard about it (I think), we usually indented subsequent lines with 3 spaces (or just one space), not with enough to make it line up.

    We're already really inconsequent with @todo comments, let's not add another style.

    Also, this mentions that we should fix this in drupal 9, but there can't be any bc break for drupal 9 - so we should either find a way to do this in a non-bc breaking way or live with it and remove the comment.

    At the very least, this should point to a d.o issue as a followup.

  2. +++ b/core/modules/rest/src/RouteProcessor/RestResourceGetRouteProcessorBC.php
    @@ -0,0 +1,80 @@
    + * Processes the BC REST routes, to ensure old.
    

    I think this is missing words, to ensure old rest route definitions still work or something similar?

  3. +++ b/core/modules/rest/src/RouteProcessor/RestResourceGetRouteProcessorBC.php
    @@ -0,0 +1,80 @@
    +   * @var array
    

    /s/array/string[]/

  4. +++ b/core/modules/rest/src/Routing/ResourceRoutes.php
    @@ -108,20 +110,20 @@ protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_
    +        // @todo Remove this in Drupal 9. @RestResource plugins should be
    +        //       given the RESTResourceConfig entity, and should generate the
    

    Same comment about indentation.

dawehner’s picture

  1. +++ b/core/modules/rest/rest.services.yml
    @@ -43,3 +43,8 @@ services:
    +  rest.route_processor_get_bc:
    +    class: \Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC
    +    arguments: ['%serializer.formats%', '@router.route_provider']
    +    tags:
    +      - { name: route_processor_outbound }
    

    ❓ I'm wondering whether we should have a flag to remove the processor, if you want to. It would be kinda nice to document the various BC things, by having configuration for all of them.

  2. +++ b/core/modules/rest/src/Plugin/ResourceBase.php
    @@ -126,13 +126,13 @@ public function routes() {
    +          $collection->add("$route_name.$method.$format_name", (new Route(''))->setOption('bc_route', TRUE));
    

    I'm wondering whether we could be more specific like: '_rest_bc_format_specific' or something like that?

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new3.68 KB
new15.49 KB

#60:

  1. A. I can never remember. Went with the 3 spaces you asked for.
    B. Done. Note that there is no way to fix this without breaking BC, which is why this comment exists. I started opening a new issue, but then I realized that this is pretty much how it was designed to work. I think that this cannot be completely resolved, not without rearchitecting more of the REST module. Such rearchitecting will only make sense after all of #2905563: REST: top priorities for Drupal 8.5.x is done, because that that point we'll have a much clearer perspective/overview of what works well and what doesn't. So, simply removed those @todos.
  2. Yep! Thanks :)
  3. Done.
wim leers’s picture

StatusFileSize
new15.23 KB
new26.42 KB

#61:

  1. Interesting, I was thinking along similar lines. This is why I went with bc_route as the route option, because it'd allow all BC routes in D8 to do something like that: so you could easily remove all BC routes.
  2. Oh I guess you're asking for config specific to this particular route. I'm not sure what the value is of having a BC flag in config for every BC route of every module. Who's realistically going to use those flags? I guess the advantage would be that any site that is new to Drupal 8.5 would have that BC layer turned off by default, so that only older D8 sites get these BC routes generated? That makes sense. Done.

    This was a lot more code to do than I thought/hoped… but such is the nature of BC layers.

Status: Needs review » Needs work

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

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new10.4 KB
new31.38 KB

I forgot to

  1. account for the case where RestServiceProvider runs before rest_update_8501() has run
  2. update UserRegistrationResource

This should reduce the number of failures. (The patch is now twice as large as #62… 😨)

The remaining failures look like this: Exception: TypeError: Argument 1 passed to Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC::__construct() must be of the type array, object given — and I have NFI why that's happening…

Status: Needs review » Needs work

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

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new31.86 KB
new344 bytes

The remaining failures look like this: Exception: TypeError: Argument 1 passed to Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC::__construct() must be of the type array, object given — and I have NFI why that's happening…

There is a parameter in DrupalKernel, which is FALSE in update.php, which stands for "Make the test pass". I set it to TRUE, and it works as expected. Just kidding :)

Long story short, in HEAD update.php passes $allowDumping = FALSE to the Drupal kernel. This results into having a ContainerBuilder around when executing update.php, which seems to not be able to resolve parameters the right way, as long its in container build state. We would rather need an actual container.
I know that Symfony doesn't actually support using a container builder on runtime, so let's switch that off.

Status: Needs review » Needs work

The last submitted patch, 67: 2858482-67.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new31.71 KB

This is identical to @dawehner's patch in #67, just rebased, so it applies again.

Status: Needs review » Needs work

The last submitted patch, 69: 2858482-67.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

So apparently @dawehner's interdiff indeed solves the problems we saw in #65 … but it causes failures in UpdatePathTestBaseTest and UpdatePathTestBaseFilledTest :(

dawehner’s picture

Mhh, I spend some time trying to debug this problem, but I haven't figured out yet where these test failures are coming from.

if ($this->containerNeedsDumping && !$this->cacheDrupalContainer($container_definition)) {
  $this->container->get('logger.factory')->get('DrupalKernel')->error('Container cannot be saved to cache.');
}

This somehow no longer gets triggered ...

wim leers’s picture

Thanks for debugging! Tricky, isn't it…

But other than the fact that it's currently not passing tests, do you prefer the approach in #63 (BC routes exist only if config enables them) and later to the one in #62 (BC routes exist always)? You requested it, but I want to make sure you like the outcome, assuming we can get it to green. If you don't like it, then we also don't need to debug it further.

wim leers’s picture

Assigned: Unassigned » dawehner
Status: Needs work » Needs review
StatusFileSize
new3.78 KB
new32.86 KB

#2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage conflicts with this in two places, in trivial ways. Rebased.

Also fixed the coding standards violations.

Assigning to @dawehner for feedback on #73, regarding the new direction he requested.

Status: Needs review » Needs work

The last submitted patch, 74: 2858482-74.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new32.81 KB
error: cannot apply binary patch to 'core/modules/rest/tests/fixtures/update/drupal-8.rest-rest_post_update_resource_granularity.php' without full index line

😭

Rerolled with --binary.

Status: Needs review » Needs work

The last submitted patch, 76: 2858482-76.patch, failed testing. View results

dawehner’s picture

But other than the fact that it's currently not passing tests, do you prefer the approach in #63 (BC routes exist only if config enables them) and later to the one in #62 (BC routes exist always)? You requested it, but I want to make sure you like the outcome, assuming we can get it to green. If you don't like it, then we also don't need to debug it further.

Yeah I think having these routes is needed, so also my review.

  1. +++ b/core/modules/rest/config/install/rest.settings.yml
    @@ -5,3 +5,10 @@
    +bc:
    +  # Before Drupal 8.5, a GET route would be created for each supported format.
    +  # This was confusing and unnecessary. The old routes are still created for BC
    +  # reasons. New Drupal installations opt out from this by default (hence this
    +  # is set to false), existing installations opt in to it.
    +  # @see rest_update_8501()
    +  format_specific_get_routes: false
    

    For contrib module reasons we need to enable those routes actually by default, otherwise new installed modules would fail as well.

  2. +++ b/core/modules/rest/src/EventSubscriber/RestConfigSubscriber.php
    @@ -37,9 +48,17 @@ public function __construct(RouteBuilderInterface $router_builder) {
    +        $this->kernel->invalidateContainer();
    

    I would love to have an opinion from @alexpott to ask him whether executing a container rebuild could be potentially horrible.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new405 bytes
new32.47 KB

Looks like reverting #67's interdiff makes tests pass :)

Status: Needs review » Needs work

The last submitted patch, 79: 2858482-79.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new32.42 KB

Forgot --binary 🤦‍♂️ Rerolled.

For contrib module reasons we need to enable those routes actually by default, otherwise new installed modules would fail as well.

For contrib module reasons we need to enable those routes actually by default, otherwise new installed modules would fail as well.

If that's the case, then do we even want/need this flag in config? Who is ever going to find out about this BC flag, and is going to manually disable it?
Is your intent to first enable the BC routes always (for both existing and new sites), and then in a future minor to not install the BC routes on new sites, because contrib modules will have had time to migrate?

wim leers’s picture

StatusFileSize
new2.32 KB
new31.82 KB
  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -53,18 +53,18 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
    -   * The config factory.
    +   * The link relation type manager used to create HTTP header links.
        *
    -   * @var \Drupal\Core\Config\ConfigFactoryInterface
    +   * @var \Drupal\Component\Plugin\PluginManagerInterface
        */
    -  protected $configFactory;
    +  protected $linkRelationTypeManager;
     
       /**
    -   * The link relation type manager used to create HTTP header links.
    +   * The config factory.
        *
    -   * @var \Drupal\Component\Plugin\PluginManagerInterface
    +   * @var \Drupal\Core\Config\ConfigFactoryInterface
        */
    -  protected $linkRelationTypeManager;
    +  protected $configFactory;
    

    Unnecessary/out of scope change: these are just being switched places. Reverted.

  2. +++ b/core/modules/rest/src/RouteProcessor/RestResourceGetRouteProcessorBC.php
    @@ -0,0 +1,79 @@
    +  public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
    +    $route_name_parts = explode('.', $route_name);
    +    // BC: the REST module originally created per-format GET routes, instead
    +    // of a single route. To minimize the surface of this BC layer, this uses
    +    // route definitions that are as empty as possible, plus an outbound route
    +    // processor.
    +    // @see \Drupal\rest\Plugin\ResourceBase::routes()
    +    if ($route_name_parts[0] === 'rest' && $route_name_parts[count($route_name_parts) - 2] === 'GET' && in_array($route_name_parts[count($route_name_parts) - 1], $this->serializerFormats, TRUE)) {
    +      array_pop($route_name_parts);
    +      $redirected_route_name = implode('.', $route_name_parts);
    +      static::overwriteRoute($route, $this->routeProvider->getRouteByName($redirected_route_name));
    +    }
    +  }
    

    This needs a deprecation error to be thrown. Expect lots of failures due to that.

The last submitted patch, 81: 2858482-81.patch, failed testing. View results

dawehner’s picture

If that's the case, then do we even want/need this flag in config? Who is ever going to find out about this BC flag, and is going to manually disable it?
Is your intent to first enable the BC routes always (for both existing and new sites), and then in a future minor to not install the BC routes on new sites, because contrib modules will have had time to migrate?

That is a good point. Well I was hoping people can disable it and run with it, if it works.

+++ b/core/core.services.yml
@@ -933,11 +933,22 @@ services:
-      - { name: route_filter, priority: 1 }
+      # The HTTP method route filter must run very early: it removes any routes
+      # whose requirements do not allow the HTTP method of the current request.
+      # Throws a 405 if no routes match the current request's HTTP method.
+      # (If it runs before content_type_header_matcher, it can ensure that that
+      # only receives routes which can have a Content-Type request header.)
+      - { name: route_filter, priority: 10 }
   content_type_header_matcher:
     class: Drupal\Core\Routing\ContentTypeHeaderMatcher
     tags:
-      - { name: route_filter }
+      # The Content-Type request header route filter must run early: it removes
+      # any routes whose requirements do not allow the Content-Type request
+      # header of the current request.
+      # Throws a 415 if no routes match the Content-Type request header of the
+      # current request, or if it has no Content-Type request header.
+      # Note it does nothing for GET requests.
+      - { name: route_filter, priority: 5 }

Everytime we change these priorities I would like to ask the panels people to test these changes ... better be safe than sorry I guess?

I think the patch looks fine right now, but I think it lacks one essential feature: Actually throw a deprecation error when someone uses the deprecated routes. Much like we want to for example add support for deprecated permissions we want to do essentially the same for routes as well. I'm happy to work on that. Overall I'm not 100% sure whether this should block this issue or not, but I think the general feature would be needed.

wim leers’s picture

StatusFileSize
new405 bytes
new32.21 KB

Ugh … so #79/#81 fixed the 2 failing tests … but causes all update path tests to fail like this:

Drupal\Tests\comment\Functional\Update\CommentUpdateTest::testCommentUpdate8101
Exception: TypeError: Argument 1 passed to Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC::__construct() must be of the type array, object given
Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC->__construct()() (Line: 37)

because the serializer.formats is apparently an object instead of an array. Just like I wrote in #65.

Sigh. Undoing #79/#81, aka repeating #67.

wim leers’s picture

That is a good point. Well I was hoping people can disable it and run with it, if it works.

So, you agree we can remove the flag? The deprecation notice is enough to encourage users to update I think.

I think the patch looks fine right now, but I think it lacks one essential feature: Actually throw a deprecation error when someone uses the deprecated routes. Much like we want to for example add support for deprecated permissions we want to do essentially the same for routes as well. I'm happy to work on that. Overall I'm not 100% sure whether this should block this issue or not, but I think the general feature would be needed.

Sounds good! 👍

The last submitted patch, 82: 2858482-82.patch, failed testing. View results

dawehner’s picture

So, you agree we can remove the flag? The deprecation notice is enough to encourage users to update I think.

Sounds sensible for me.

wim leers’s picture

Assigned: dawehner » wim leers
Status: Needs review » Needs work

Alright, on it then. That will also remove the need for the container rebuild, which means the question in #78.2 for Alex Pott is no longer necessary.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new19.64 KB
new15.85 KB

That allowed for a massive simplification: from 18 files changed, 245 insertions(+), 60 deletions(-) to 8 files changed, 139 insertions(+), 42 deletions(-).

The set of changes now looks much more coherent, and hence much easier to review :)

The last submitted patch, 85: 2858482-84.patch, failed testing. View results

wim leers’s picture

StatusFileSize
new24.77 KB
new40.57 KB

IDK why, but DeprecationListener doesn't seem to work for me (not in PHPStorm, not using phpunit directly, and yes, with updated phpunit.xml and up-to-date vendor). Is this because class EntityResourceTestBase extends class ResourceTestBase extends BrowserTestBase?

(I also tried using @deprecatedException + @group legacy, but that also failed. That'd be a nightmare here though, because the exception message is different for every subclass. Trying that is also how I found #2928645: \Drupal\Tests\migrate\Kernel\Plugin\MigrationDirectoryTest uses @expectedDeprecationMessage.)

wim leers’s picture

Assigned: wim leers » Unassigned

The last submitted patch, 90: 2858482-90.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 92: 2858482-92.patch, failed testing. View results

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new40 KB
new1.23 KB

Things surprisingly work well, when you are doing them right.

wim leers’s picture

Things surprisingly work well, when you are doing them right.

😭😇😱

:D

I'm an idiot. So glad you caught that. I could've debugged that for a few more hours before finding that :D

Status: Needs review » Needs work

The last submitted patch, 96: 2858482-96.patch, failed testing. View results

wim leers’s picture

Hm, that also didn't work apparently? :( Did I get something else wrong?

dawehner’s picture

Oh, weird

benjy’s picture

Applying this patch with 8.4.x breaks one of my tests. Afterwards I have the XML route enabled even though my config looks like this:

uuid: 6a0edca9-4a10-4a87-9ba3-8af3d3d1d0a7
langcode: en
status: true
dependencies:
  module:
    - serialization
    - unearthed_forum
    - user
id: unearthed_forum_subscription
plugin_id: unearthed_forum_subscription
granularity: resource
configuration:
  methods:
    - GET
    - POST
  formats:
    - json
  authentication:
    - cookie

I end up with the error No route found for the specified format html. in a test when running the following code

$url = Url::fromUri('internal:/api/forum/subscriptions/' . $this->question->id()

This is because AcceptHeaderMatcher::filter can no longer detect the correct default format since there are multiple routes enabled.

// If the route has a format requirement, then verify that the
// resource has it.
$format_requirement = $route->getRequirement('_format');
if ($format_requirement && !in_array($format_requirement, $rest_resource_config->getFormats($method))) {
  continue;
}

This is the hunk of code that was removed that causes the issue. Brining that back fixes my issue.

wim leers’s picture

wim leers’s picture

StatusFileSize
new1.71 KB
new40.11 KB

@benjy: Thanks for testing this!

Afterwards I have the XML route enabled even though

Aha!

The reason is that we generate BC routes for all formats, even formats that aren't enabled. That's easy enough to fix :)


Let's start by adding a failing test that detects the regression reported in #101.

wim leers’s picture

StatusFileSize
new1.87 KB
new40.51 KB

And the fix!

The last submitted patch, 102: 2858482-102.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 104: 2858482-104.patch, failed testing. View results

benjy’s picture

+++ b/core/modules/rest/src/Plugin/ResourceBase.php
@@ -130,7 +130,10 @@ public function routes() {
+          $collection->add("$route_name.$method.$format_name", (new Route(''))

Not sure how this is meant to work, because it's a new route it loses the "methods" and therefore never gets a route in ResourceRoutes.php.

This is what worked for me.

            $format_route = clone $route;
            $format_route
              ->setRequirement('_format', $format_name)
              ->setOption('bc_route', TRUE);
            $collection->add("$route_name.$method.$format_name", $format_route);
wim leers’s picture

Not sure how this is meant to work, because it's a new route it loses the "methods" and therefore never gets a route in ResourceRoutes.php.

It works fine actually, because:

if (($methods && ($method = $methods[0]) && $supported_formats = $rest_resource_config->getFormats($method)) || $route->hasOption('bc_route')) {

That || $route->hasOption('bc_route' is what makes it work. It's intentional that the route definition of this BC route is as empty as possible, because \Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC takes care of automatically filling it with the successor route's definition.

Note that #104 is failing not due to the changes I made in those last few patches, it already was failing, due to the BC @trigger_error() stuff.

So: are you sure it's still broken for you?

benjy’s picture

Ahh i see, i'm using this patch - https://www.drupal.org/project/drupal/issues/1927648?page=1#comment-1224... which didn't have the the OR, but is the best I have that works against 8.4.x.

Is it worth adding a comment to new Route('') because just reading it, it feels wrong?

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new878 bytes
new40.53 KB

Added such a comment.

benjy’s picture

+++ b/core/modules/rest/src/Plugin/ResourceBase.php
+      // route definitions that are as empty as possible, plus an outbound route
+      // processor.


+          $collection->add("$route_name.$method.$format_name", (new Route(''))

It's the "" argument to Route that seems off, I know we mention "empty as possible" but it still isn't immediately clear reading the code. Could we have a new version of the route object to represent deprecated routes? Not sure how they are stored in the database, would work if they were serialized?

wim leers’s picture

Woah, #110 suddenly passed, without the 413 fails … because something must've changed in the handling of deprecations. Well, YAY, that means we can finally continue here :)


#111: I understand that the first argument to the Route constructor being the empty string looks confusing, but otherwise we still end up putting metadata in those routes, which we specifically don't want to.

I don't know what you're asking for when you say Could we have a new version of the route object to represent deprecated routes? — could you explain that a bit more? I also don't understand what you're getting at when you say Not sure how they are stored in the database, would work if they were serialized?.

benjy’s picture

I don't know what you're asking for when you say Could we have a new version of the route object to represent deprecated routes? — could you explain that a bit more? I also don't understand what you're getting at when you say Not sure how they are stored in the database, would work if they were serialized?.

Currently we setup the route object each time we want to add a BC route with the right metadata, e.g. with (new Route(''))->setOption('bc_route', TRUE);. My point was, if the route is only serialised in the database to store basic route info, we could just as easily have a new class that encapsulates the BC variation for us.

class BcRoute extends Route {
  public function __construct(.....) {
    parent::__construct('', ....);
    $this->setOption('bc_route', TRUE);
  }
}

Then, in the patch we simply do, new BcRoute(). It would give us somewhere to document this behaviour and it's only a CMD+click away when you're reading the route generation code.

wim leers’s picture

StatusFileSize
new2.25 KB
new41.78 KB

#113: AHHHHH! I love that! 😍

Done.

Status: Needs review » Needs work

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

wim leers’s picture

Status: Needs work » Needs review

Random infra fail.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

I really like that too, great idea!

wim leers’s picture

WOOOT!

benjy’s picture

Great, patch looks good. Couple of super minor PHPCS things.

  1. +++ b/core/lib/Drupal/Core/Routing/BcRoute.php
    @@ -0,0 +1,29 @@
    + * F.e. see \Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC.
    

    Should this be @see ?

  2. +++ b/core/lib/Drupal/Core/Routing/BcRoute.php
    @@ -0,0 +1,29 @@
    \ No newline at end of file
    

    Missing a new line in BcRoute.

wim leers’s picture

StatusFileSize
new450 bytes
new41.76 KB
  1. This was intentional, it's meant to be just an example, not a must-see reference implementation. I've struggled to choose between what's currently in the patch and @see though. IDK what's best. The sad thing is that @see must be on a line of its own, otherwise I'd do it inline here.
  2. 👍 Fixed!

I'm glad we're down to this level of nitpicking :D

Thanks for all your detailed feedback so far, @benjy, much appreciated! 👌

benjy’s picture

1. Yeah, I don't have any good suggestions there. RTBC +1 though.

wim leers’s picture

Priority: Normal » Major

This is very important for maintainability, and the size of the patch reflects the complexity. Therefore, bumping to major.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 120: 2858482-120.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new41.75 KB

#2916740: Add generic entity actions conflicted with this, in DeprecationListenerTrait. Rebased.

wim leers’s picture

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 124: 2858482-124.patch, failed testing. View results

wim leers’s picture

Are you kidding me? :(

The 'rest.entity.entity_test.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead: 1x

My bet is that #2934336: Update symfony/phpunit-bridge to the latest released version caused this :(

wim leers’s picture

Assigned: Unassigned » wim leers
17:59:20 <alexpott> @WimLeers: The upgrade now make deprecation visible again
17:59:47 <alexpott> @WimLeers: your tests are triggering a deprecation "The 'rest.entity.entity_test.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead"
18:00:08 <alexpott> WimLeers: lol I've been slacked adding @ in front of things
18:03:47 <WimLeers> alexpott: but I'm also updating the DeprecationListenerTrait …
18:03:56 <WimLeers> alexpott: neither dawehner nor I were able to figure this out
18:04:08 <WimLeers> alexpott: I'm hoping you see what obvious mistake we made :D
18:05:14 <alexpott> WimLeers: I'm not sure about adding all that stuff to getSkippedDeprecations - we really shouldn't be adding to that.
18:06:57 <alexpott> WimLeers: loooking
18:09:19 <WimLeers> alexpott: thanks, much appreciate it
18:13:35 <alexpott> WimLeers: you're missing a fullstop
18:15:13 <alexpott> WimLeers: for reasons I don't quite know Symfony strips the final fullstop off the message
18:15:42 <alexpott> WimLeers: but "The 'rest.entity.entity_test.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead.", works for me
18:16:22 <alexpott> WimLeers: so yay deprecation testing is truly fixed by 2934336
18:16:32 <WimLeers> alexpott: FML
18:16:36 <WimLeers> alexpott++
18:16:39 <WimLeers> alexpott++
18:16:46 — alexpott broke it getting PHP7.2 fixed
18:16:50 <WimLeers> alexpott: So that's why dawehner and I weren't able to spot it
18:16:55 <WimLeers> alexpott: because the strings DO match
18:17:02 <WimLeers> alexpott: it's just that Symfony is modifying them…
18:17:06 <WimLeers> gahhhhhh
18:17:12 <alexpott> WimLeers: yeah I think we should submit a PR to Symfony to stop that
18:17:28 <alexpott> WimLeers: I'll do that
18:17:28 <WimLeers> alexpott: well if that code is there, it's probably intentional
18:17:40 <alexpott> WimLeers: it is to make it a sentence

🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new48.35 KB
new41.88 KB
wim leers’s picture

To improve this in the future, @alexpott created https://github.com/symfony/symfony/pull/25757

wim leers’s picture

And he even created a Drupal port of it, in case Symfony doesn't merge his PR: #2935755: Add a trait to allow dynamic setting of expected deprecations.

wim leers’s picture

#2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle() landed earlier today.

Like that issue, this issue is blocking #1927648: Allow creation of file entities from binary data via REST requests. I said in #125 already that this was a hard blocker, but the latest patch (#1927648-501: Allow creation of file entities from binary data via REST requests) is now truly based on this.

larowlan’s picture

I think we should wait on #2935755: Add a trait to allow dynamic setting of expected deprecations here if that prevents adding all those expected deprecations - thoughts?

wim leers’s picture

I think we shouldn't because this blocks #1927648: Allow creation of file entities from binary data via REST requests. It'd be trivial to simplify \Drupal\Tests\Listeners\DeprecationListenerTrait::getSkippedDeprecations() once #2935755: Add a trait to allow dynamic setting of expected deprecations lands. I'd be happy to guarantee that happens.

dawehner’s picture

I think iteration on existing code should be easy, so I agree with @Wim Leers that we shouldn't wait.

Also wow, I would have never figured out the deprecation message bit.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/core.services.yml
    @@ -935,11 +935,22 @@ services:
    +      # (If it runs before content_type_header_matcher, it can ensure that that
    

    nit: 'that that' - 'that it'?

  2. +++ b/core/lib/Drupal/Core/Routing/BcRoute.php
    @@ -0,0 +1,29 @@
    + * F.e. see \Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC.
    

    @see ? instead of F.e see

  3. +++ b/core/modules/rest/src/RouteProcessor/RestResourceGetRouteProcessorBC.php
    @@ -0,0 +1,80 @@
    +    $route_name_parts = explode('.', $route_name);
    

    Should we use the third parameter here?

  4. +++ b/core/modules/rest/src/RouteProcessor/RestResourceGetRouteProcessorBC.php
    @@ -0,0 +1,80 @@
    +    if ($route_name_parts[0] === 'rest' && $route_name_parts[count($route_name_parts) - 2] === 'GET' && in_array($route_name_parts[count($route_name_parts) - 1], $this->serializerFormats, TRUE)) {
    

    would using list() here to create named variables make this easier to understand?

  5. +++ b/core/modules/rest/src/RouteProcessor/RestResourceGetRouteProcessorBC.php
    @@ -0,0 +1,80 @@
    +    $target_route->setPath($source_route->getPath());
    +    $target_route->setDefaults($source_route->getDefaults());
    +    $target_route->setRequirements($source_route->getRequirements());
    +    $target_route->setOptions($source_route->getOptions());
    +    $target_route->setHost($source_route->getHost());
    +    $target_route->setSchemes($source_route->getSchemes());
    +    $target_route->setMethods($source_route->getMethods());
    

    Would it be worth creating a factory method on BcRoute e.g.::fromRoute so others can use this, at present it is limited to this processor.

    In addition, that could make use of scope advantages given its a sub-class.

  6. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -639,15 +640,32 @@ public function testGet() {
    +    try {
    +      $bc_route_other_format->toString(TRUE);
    +      $this->assertTrue(FALSE);
    +    }
    +    catch (RouteNotFoundException $e) {
    +      $this->assertTrue(TRUE);
    

    shouldn't this use setExpectedException

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new6.47 KB
new42.05 KB
  1. Heh, non-native-speaker mistake. Fixed!
  2. No, because we're interested in 3 different subsections: the very first, the last, and the second last. We don't care about the subsections between the first and the second last. (Also, see next point.)
  3. Yes! Good call. It does make the code far more verbose though, but for a good reason probably :) We can't, because this works for any REST resource plugin, and the routes generated are of the form rest.PLUGIN_ID.METHOD.FORMAT. So rest.entity.node.GET.json or rest.dblog.GET.json. Note how explode('.', …) would result in 5 parts for the former, and 4 parts for the latter.
  4. I like that idea! But AFAICT it sets the wrong expectation. The point of BcRoute is that it's an as-empty-as-possible route definition. X::fromY() implies constructing a X from Y, but that implies that X inherits all metadata/definition data from Y. Which is exactly what we don't want here.
    It would still work, but it'd encourage the wrong usage. Unless … we also override serialize(), to guarantee that it'll always be an empty definition that gets stored in the router.
    Did that, curious to hear what you think.
  5. Sure. (But this means we can't add more assertions after this. And adding an entirely new test method for this will slow down every REST test. For now this is fine though!)
dawehner’s picture

It would still work, but it'd encourage the wrong usage. Unless … we also override serialize(), to guarantee that it'll always be an empty definition that gets stored in the router.

In the same way we could limit the setters. Actually though, I think we should remove that given that we don't know the usecases people might have when using the class?

larowlan’s picture

Yeah agree, I think we should wait until the dedicated issue for depreciation of routes. For now let's leave the static on the outbound processor. That way we can work out the best way to do it without holding this up.

wim leers’s picture

I’m at 🏓, can somebody reupload #129 and RTBC? Thanks!

larowlan’s picture

Version: 8.5.x-dev » 8.6.x-dev
Status: Reviewed & tested by the community » Needs work
Related issues: +#2928750: Allow to deprecate routes

I think we need #137 minus #136.5, not #129

No rush Wim, I'll keep my eye on this across the weekend.

Added deprecate issue to related.

We also need a follow-up filed to remove all usages of the deprecated GET routes from core, and then remove the messages from the allowed deprecation messages list. I'd love to see that fixed as soon as possible just to keep that list trimmed and terrific. But obviously, let's get through the alpha deadline first.

Sorry for the misdirection on the BcRoute factory, you're too efficient - I posed the question and was expecting discussion first :)

Ooh, we have an 8.6.x branch now.

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new5.34 KB
new1.45 KB
new41.03 KB

I think we need #137 minus #136.5, not #129

Right, I realized that long after posting #140!

Sorry for the misdirection on the BcRoute factory, you're too efficient - I posed the question and was expecting discussion first :)

No problem at all! I don't mind exploring these things. It took me <15 minutes to write the implementation. Discussing an actual implementation makes the discussion much more concrete, and hence more effective. It was worth exploring!

DO NOT READ THIS INTERDIFF OR PATCH, I messed things up. Don't skip reading comments, just skip the interdiffs/patch on this comment. Redid the interdiffs/patch in #146.

wim leers’s picture

We also need a follow-up filed to remove all usages of the deprecated GET routes from core, and then remove the messages from the allowed deprecation messages list. I'd love to see that fixed as soon as possible just to keep that list trimmed and terrific. But obviously, let's get through the alpha deadline first.

There are no usages! Only in tests:

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -693,15 +694,27 @@ public function testGet() {
+    // BC: Format-specific GET routes are deprecated. They are available on both
+    // new and old sites, but trigger deprecation notices.
+    $bc_route = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . static::$format, $url->getRouteParameters(), $url->getOptions());
+    $bc_route->setUrlGenerator($this->container->get('url_generator'));
+    $this->assertSame($url->toString(TRUE)->getGeneratedUrl(), $bc_route->toString(TRUE)->getGeneratedUrl());
+    // Verify no format-specific GET BC routes are created for other formats.
+    $other_format = static::$format === 'json' ? 'xml' : 'json';
+    $bc_route_other_format = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . $other_format, $url->getRouteParameters(), $url->getOptions());
+    $bc_route_other_format->setUrlGenerator($this->container->get('url_generator'));
+    $this->setExpectedException(RouteNotFoundException::class);
+    $bc_route_other_format->toString(TRUE);

This is the only way the deprecation error is being triggered, and it's the reason we're updating \Drupal\Tests\Listeners\DeprecationListenerTrait. That is why I worked with @alexpott to get #2935755: Add a trait to allow dynamic setting of expected deprecations going. Once that lands, we can change the test coverage to use expectDeprecation($message) instead. Without that, we'd have to have a method that every subclass of EntityResourceTestBase needs to override, to be able to set the appropriate @expectedDeprecation docblock.

IOW: as soon as #2935755: Add a trait to allow dynamic setting of expected deprecations lands, we can simplify DeprecationListenerTrait again!

+++ b/core/modules/dblog/tests/src/Functional/DbLogResourceTest.php
@@ -62,7 +62,7 @@ public function testWatchdog() {
-    $url = Url::fromRoute('rest.dblog.GET.' . static::$format, ['id' => $id, '_format' => static::$format]);
+    $url = Url::fromRoute('rest.dblog.GET', ['id' => $id, '_format' => static::$format]);

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -693,15 +694,27 @@ public function testGet() {
-    $url = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . static::$format);
+    $url = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET');

+++ b/core/modules/user/src/Tests/RestRegisterUserTest.php
@@ -166,7 +167,7 @@ protected function registerUser($name, $include_password = TRUE) {
-    $this->httpRequest('/user/register', 'POST', $serialized, 'application/hal+json');
+    $this->httpRequest(Url::fromRoute('rest.user_registration.POST', ['_format' => 'hal_json']), 'POST', $serialized, 'application/hal+json');

These are all existing uses, being updated.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 142: 2858482-142.patch, failed testing. View results

wim leers’s picture

I completely messed up #142.

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new2.69 KB
new4.14 KB
new41.8 KB

So, #142 was completely wrong. This completely ignores #142. Again with interdiffs from #129 and #137, but this time … correct. 🤞

dawehner’s picture

Great work @Wim Leers!

larowlan’s picture

Added review credits

larowlan’s picture

Version: 8.6.x-dev » 8.5.x-dev
Status: Reviewed & tested by the community » Fixed

Committed as b4a0230 and pushed to 8.6.x

Cherry-picked as b15791c and pushed to 8.5.x

Published change record

Thanks all, see you in #2928750: Allow to deprecate routes

larowlan’s picture

Can we get the follow up for the offspring of this issue and #2935755: Add a trait to allow dynamic setting of expected deprecations

  • larowlan committed b4a0230 on 8.6.x
    Issue #2858482 by Wim Leers, dawehner, benjy, larowlan, tedbow,...

  • larowlan committed b15791c on 8.5.x
    Issue #2858482 by Wim Leers, dawehner, benjy, larowlan, tedbow,...

Status: Fixed » Closed (fixed)

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

wim leers’s picture

Issue tags: +Routing