Steps to reproduce

  1. Create a custom rest resource (code example here)
  2. Enable the new rest resource with restui, allowing get, patch, post, and put HTTP verbs
    .
  3. Set permissions permitting all users to access the resource
    .
  4. Send 1 http request to your resource for each http verb. See that get, patch, and put all succeed. See that post fails with a message:
    {
      "message": "No route found for \"POST /todo/\": Method Not Allowed (Allow: PATCH, GET, PUT)"
    }
    

    .
    .

Expected Result

That my custom resource would accept all http verbs, including post.

Actual result

get, patch, and put all work, but post fails with the error:

{
  "message": "No route found for \"POST /todo/\": Method Not Allowed (Allow: PATCH, GET, PUT)"
}

Video Demonstration

http://www.youtube.com/watch?v=Hta2FhIrVMk

Relevant code

https://gist.github.com/MKorostoff/a1d8ad7f72f2d4fc051dd6dfc89eed6c

Comments

MKorostoff created an issue. See original summary.

MKorostoff’s picture

Issue summary: View changes
StatusFileSize
new74.26 KB
new58.48 KB
dawehner’s picture

Can you try to post to /todo_rest_resource ?
$create_path = isset($definition['uri_paths']['https://www.drupal.org/link-relations/create']) ? $definition['uri_paths']['https://www.drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/'); is the line that creates the needed route.

MKorostoff’s picture

StatusFileSize
new44.28 KB

@dawenhner, thanks, that works! I am able to post to /todo_rest_resource

.

As to the second point, regarding $create_path—where are you suggesting I put that line? I can see that line is already present in my base class at \Drupal\rest\Plugin\ResourceBase::routes()

Also, can you help me understand why get, patch, and put all respond on the url /todo but post (and only post) responds on the url /todo_rest_resource?

dawehner’s picture

Status: Active » Fixed

#2800871: Create a post method in Drupal 8: how to receive data? shows you can you use a different path for the POST route.

MKorostoff’s picture

Title: Cannot post on custom rest resource » REST uses a different URL for POST requests as compared to every other http method.
Status: Fixed » Active
StatusFileSize
new249.3 KB

Looking at this a little more closely, I actually suspect this is a bug. As you can see in \Drupal\rest\Plugin\ResourceBase::routes(), when using the POST method, the path is set explicitly to the pluginID:

screenshot of code

The trouble is, POST is the only method that receives this treatment. GET, PATCH, and HEAD all use the 'canonical path.' I can't seem to think of a reason why it would ever be desirable to have POST requests sent to one URL, but all other request types sent to a different URL.

For my custom resource, it's easy enough to just override the routes() method. However, I think this starts to look a little more serious is when accessing core resources. For instance, to read, update, or delete a node you must request /node/:nid. To create a node you must request /entity/node. I think this is pretty unintuitive, and I spent a long time figuring out how create nodes with Drupal rest for exactly this reason.

The patch would be simple enough (just delete line 108 in the code block in my screenshot above). There's two things I worry about, however. First, is that I simply don't have the intimate familiarity with this code to say for sure that this wouldn't break something else. Second, I think it would be prudent to provide reverse compatibility so those using REST today do not suddenly have their resource urls change.

I've updated the issue title to reflect this new analysis.

dawehner’s picture

Well, its easy, GET/PATCH/HEAD ... all deal with a canonical URL, like the url to the entity you are saving. This one doesn't exist yet for newly created entities, which is the purpose of a POST request. Given that we need two different paths for them.

MKorostoff’s picture

@dawehner yeah, of course PATCH and POST operations can't happen at the exact same URL. What I'm talking about is the base path, the part before the entity ID argument. If you GET a node from example.com/node/1, then you'd expect to be able to POST a node to example.com/node. At very least, I would expect for the first URL hunk to remain the same across all http verbs—something like /node/create would not be too unusual.

I don't think I have ever encountered a REST API that doesn't follow this pattern. This was the pattern in Drupal 7 restws as well as Drupal 7 services. Across a wide variety of products and languages, this is a very well established convention. Compare, for instance, these extremely popular REST APIs: stripe, twitter, google docs

wim leers’s picture

Assigned: Unassigned » wim leers
Issue tags: +Needs documentation

To answer #6: Yes, in all cases it uses the "canonical path", and only in the POST case, it uses the "create path". You're saying the path is explicitly set to the plugin ID, but this is not true.

Look at the code:

    $canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}';
    $create_path = isset($definition['uri_paths']['https://www.drupal.org/link-relations/create']) ? $definition['uri_paths']['https://www.drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/');

    …

    foreach ($methods as $method) {
      $route = $this->getBaseRoute($canonical_path, $method);

      switch ($method) {
        case 'POST':
          $route->setPath($create_path);
          …
          break;

        case 'PATCH':
          …
          break;

        case 'GET':
        case 'HEAD':
          …
          break;

        default:
          …
          break;
      }
    }

In other words:

  1. we default the REST resource's route's path to the canonical path
  2. but for POST, we override it: $route->setPath($create_path
  3. and the default canonical & create paths are both based on the plugin ID. The default canonical path is /{plugin_id}/{resource_id}, the default create path is /{plugin_id}

So, as long as you don't specify a canonical path, all URLs will make sense. But … you did specify a canonical path:

 *   uri_paths = {
 *     “canonical” = “/todo”
 *   }

So, if you want your "create path" (POST) to match, then you need to change that to:

 *   uri_paths = {
 *     “canonical” = “/todo”
 *     "https://www.drupal.org/link-relations/create" = "/todo"
 *   }

Also: your canonical path looks incorrect, because it doesn't allow for an ID to be specified. It should probably be /todo/{todo_id}. So you probably want something like:

 *   uri_paths = {
 *     “canonical” = “/todo/{todo_id}”
 *     "https://www.drupal.org/link-relations/create" = "/todo"
 *   }

I trust/hope this answers all your questions. Thanks for reporting this!


So, I don't there is a bug in the code here. But, what clearly is lacking, is documentation. So, tagging for that, and working on that.

wim leers’s picture

Status: Active » Fixed
Issue tags: -Needs documentation
wim leers’s picture

Issue tags: +Documentation
wim leers’s picture

Title: REST uses a different URL for POST requests as compared to every other http method. » Documentation: REST resource uses a different URI path for POST than for GET|PATCH|DELETE
MKorostoff’s picture

Thanks! Looks great :-)

wim leers’s picture

You're very welcome! Glad you're happy with this solution :)

Status: Fixed » Closed (fixed)

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

rudolfbyker’s picture

Version: 8.1.9 » 10.0.x-dev

Did this change in D9? Do we need to update the docs again?

erfekkes’s picture

in Drupal 9, the create link for POST operations has changed, as seen in ResourceBase.php

public function routes() {
    $collection = new RouteCollection();

    $definition = $this->getPluginDefinition();
    $canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}';
    $create_path = isset($definition['uri_paths']['create']) ? $definition['uri_paths']['create'] : '/' . strtr($this->pluginId, ':', '/');

    $route_name = strtr($this->pluginId, ':', '.');

    $methods = $this->availableMethods();
    foreach ($methods as $method) {
      $path = $method === 'POST'
        ? $create_path
        : $canonical_path;
      $route = $this->getBaseRoute($path, $method);

      // Note that '_format' and '_content_type_format' route requirements are
      // added in ResourceRoutes::getRoutesForResourceConfig().
      $collection->add("$route_name.$method", $route);
    }

    return $collection;
  }

In order to get the correct uri for POST commands, now in the @RestResource annotation, in uri_paths, the key "create" has to be used referring to the uri - similar to the approach for the canonical uri.

anoopjohn’s picture

I think the public documentation has to be updated as well along with API documentation for Drupal 9. Couldn't find a reference to the change from

https://www.drupal.org/docs/drupal-apis/restful-web-services-api/restful...
https://api.drupal.org/api/drupal/core%21core.api.php/group/third_party/...
https://api.drupal.org/api/drupal/core%21modules%21rest%21src%21Annotati...

Not exactly sure where to document this change.