Problem/Motivation

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. — fixed by #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
  2. ResourceBase generates one route per method for POST/PATCH/DELETE, but one route per method+format for GET. This should be made consistent. — fixed by #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make 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. — fixed by #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
  4. ResourceBase sets _content_type_format for PATCH/POST, but it should actually also set _format. — fixed by #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
  5. ResourceBase sets _content_type_format not to the allowed formats for that particular REST resource (as per the rest.settings configuration), but to all acceptable serialization formats, and then has validation for it in \Drupal\rest\RequestHandler::handle(). — fixed by #2826407: PATCH + POST allowed format validation happens in RequestHandler::handle(), rather than in the routing system
  6. ResourceRoutes::alter() is doing configuration verification. — fixed by #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
  7. 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(). — fixed by #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent
  8. ResourceRoutes::alter() sets _csrf_request_header_token on all routes, including GET routes. This is harmless, but it causes extra, unnecessary computations on those requests. And it makes the route more confusing to debug. — fixed by #2869426: EntityResource should add _entity_access requirement to REST routes
  9. RequestHandler::handle() assumes _format is always set, and always has a single value — and in case it's not set, its ultimate solution is "just pick JSON". But it's not guaranteed to have a single value… nor does it even always have a value. And in fact, PATCH/POST routes do not have _format set. So any PATCH/POST request will actually result in a JSON response, even if you explicitly specify ?_format=xml! — fixed by #2662284: Return complete entity after successful PATCH
  10. Handling of 4xx responses is done in RequestHandler::handle() instead of in a KernelEvents::EXCEPTION event subscriber. This prevents Basic Auth 401 responses from being sent, and means REST has its own system in parallel to Symfony's/Drupal core's. — fixed by #2739617: Make it easier to write on4xx() exception subscribers + #2805281: ?_format=hal_json error responses are application/json, yet should be application/hal+json + #2813853: RequestHandler has its own error handling rather than leaving it to KernelEvents::EXCEPTION event subscribers
  11. RequestHandler is hard to understand, and is messy. — fixed by #2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle()
  12. RequestHandler has fairly complex logic to deal with HEAD requests. — fixed by #2775479: Try to remove the "map HEAD to GET" logic in \Drupal\rest\RequestHandler::handle()

Clearly, this leaves us in a state where we keep finding bizarre bugs.

Proposed resolution

Refactor REST's routing for better maintainability.

Remaining tasks

See child issues.

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

wim leers’s picture

Title: REST routing is brittle, riddled with bugs » Refactor REST routing to address its brittleness
klausi’s picture

_format is the Accept header a client sends. It is irrelevant on POST/PATCH/DELETE because drupal will look at the Content-type header that is set for the data that comes with the request. That's also the reason why there are multiple routes for GET.

klausi’s picture

Oh, and for DELETE _format and _content_type_format is irrelevant because no data will be send or received from the server.

dawehner’s picture

While fixing things here is nice, we should always try to ensure that we don't break any existing plugins out there. This could be always quite tricky.

wim leers’s picture

#4: Well, it's totally valid to send data in one format and request it in a different format. And drupal will look at the Content-type header that is set for the data that comes with the request is simply untrue: it uses that format only for deserializing incoming data, not for the response. The REST module does not currently support POST/PATCH requests that have actual response bodies, and not just a status, and it's because of that that this was not noticed until #2546216: Return entity object in REST response body after successful POST and #2662284: Return complete entity after successful PATCH. So it's understandable that it's in the state it is in, but it's not correct.
EDIT: from RequestHandler::handle():

    // Deserialize incoming data if available.
    …
    if (!empty($received)) {
      $format = $request->getContentType();
      …
    }

    …

    // Invoke the operation on the resource plugin.
    // All REST routes are restricted to exactly one format, so instead of
    // parsing it out of the Accept headers again, we can simply retrieve the
    // format requirement. If there is no format associated, just pick JSON.
    $format = $route_match->getRouteObject()->getRequirement('_format') ?: 'json';
    try {
      $response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
      …

#5: I also don't know a use case for DELETE, but perhaps there is a valid use case to have a request or response body for DELETE requests? This is the one I'm least concerned about for sure.


#6: This is indeed my biggest fear, that we must continue to emulate the current bugs/quirks to not break BC.

dawehner’s picture

Category: Task » Plan

Let's make that a plan and work on small things which certainly don't break BC.

kriboogh’s picture

Just out of curiosity, why the _format query parameter on a GET request? can't this be extracted from the Accept header, cause that's where you specify your desired format no?

wim leers’s picture

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.

wim leers’s picture

Issue summary: View changes
  1. Handling of 4xx responses is done in RequestHandler::handle() instead of in a KernelEvents::EXCEPTION event subscriber. This prevents Basic Auth 401 responses from being sent, and means REST has its own system in parallel to Symfony's/Drupal core's.

This was fixed by the combination of:

  1. #2739617: Make it easier to write on4xx() exception subscribers
  2. #2805281: ?_format=hal_json error responses are application/json, yet should be application/hal+json
  3. #2813853: RequestHandler has its own error handling rather than leaving it to KernelEvents::EXCEPTION event subscribers

One less thing to do here :)

wim leers’s picture

Issue summary: View changes
Related issues:
  1. RequestHandler::handle() assumes _format is always set, and always has a single value — and in case it's not set, its ultimate solution is "just pick JSON". But it's not guaranteed to have a single value… nor does it even always have a value. And in fact, PATCH/POST routes do not have _format set. So any PATCH/POST request will actually result in a JSON response, even if you explicitly specify ?_format=xml!

This was fixed by #2662284: Return complete entity after successful PATCH.

wim leers’s picture

Issue summary: View changes
  1. ResourceRoutes::alter() sets _access_rest_csrf on all routes, including GET routes. This is harmless, but it causes extra, unnecessary computations on those requests. And it makes the route more confusing to debug.

#2753681: Move CSRF header token out of REST module so that user module can use it, as well as any contrib module caused a change in this one. It's now _csrf_request_header_token. The root cause has not been addressed yet though. IS updated.

wim leers’s picture

Issue tags: +maintainability

I checked it again, and points 1 through 8 all still need to be addressed.

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

  1. ResourceBase sets _content_type_format not to the allowed formats for that particular REST resource (as per the rest.settings configuration), but to all acceptable serialization formats, and then has validation for it in \Drupal\rest\RequestHandler::handle().

This is being worked on at #2826407: PATCH + POST allowed format validation happens in RequestHandler::handle(), rather than in the routing system.

wim leers’s picture

Issue summary: View changes

Added:

  1. RequestHandler is hard to understand, and is messy. — fixed by #2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle()
wim leers’s picture

Issue summary: View changes

Added:

  1. RequestHandler has fairly complex logic to deal with HEAD requests. — fixed by #2775479: Try to remove the "map HEAD to GET" logic in \Drupal\rest\RequestHandler::handle()
wim leers’s picture

wim leers’s picture

Issue summary: View changes

Opened #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent to fix points 1+2+3+4 and 6+7. Why fix so many points in a single issue? Because they're all very much related; they all depend on the same few lines of code in ResourceBase.

This means that only point 8 does not yet have an issue to fix it:

  1. ResourceRoutes::alter() sets _csrf_request_header_token on all routes, including GET routes. This is harmless, but it causes extra, unnecessary computations on those requests. And it makes the route more confusing to debug.

I'm also removing point 13, with the ellipsis. Because once the first 12 issues are done, I think REST routing is sufficiently non-brittle.

wim leers’s picture

Issue summary: View changes
wim leers’s picture

wim leers’s picture

Title: Refactor REST routing to address its brittleness » [PP-3] Refactor REST routing to address its brittleness
Status: Active » Postponed

Per #25.

wim leers’s picture

wim leers’s picture

Title: [PP-3] Refactor REST routing to address its brittleness » [PP-2] Refactor REST routing to address its brittleness

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-2] Refactor REST routing to address its brittleness » [PP-3] Refactor REST routing to address its brittleness
wim leers’s picture

This is the only one I didn't have an issue for yet:

  1. ResourceRoutes::alter() sets _csrf_request_header_token on all routes, including GET routes. This is harmless, but it causes extra, unnecessary computations on those requests. And it makes the route more confusing to debug.

But as of #2869426-28: EntityResource should add _entity_access requirement to REST routes, that now has an issue+patch, and it's actually proven to be less harmless than I thought.

wim leers’s picture

wim leers’s picture

Issue summary: View changes

Forgot to strike through point 5, per #32.

wim leers’s picture

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

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

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.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] Refactor REST routing to address its brittleness » Refactor REST routing to address its brittleness
Status: Postponed » Fixed

YAY!!!!!!! #2869426: EntityResource should add _entity_access requirement to REST routes just landed! Which means this is now done!

Status: Fixed » Closed (fixed)

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