Skip to content

Routing with POST method when relative route is / #19064

Closed
@iruizmar

Description

@iruizmar

Hey,

I found something weird... I have a routing system with a modular structure, inside my general routing.yml there's an api_routing.yml and, inside it, there's one .yml for every 'module' of my application, those are:

benefits_routing.yml
entities_routing.yml
user_routing.yml

Focusing on benefits, the general routing.yml take all /api routes to go to api_routing.yml, that takes all /benefits routes to go to benefits_routing.yml. Thus, if routes inside my benefits_routing.yml are:

benefits_routing.yml
api_list_benefits:
    path: /
    defaults: {_controller: "AppBundle:Benefits:list" }
    methods: [GET]
api_new_benefits:
    path: /
    defaults: {_controller: "AppBundle:Benefits:new" }
    methods: [POST]

Then, if I make a POST request to http://domain/api/benefits, the router should match to the second route, right? Nope.

When I post to '/api/benefits' router throw a 405. Though, if I post to '/api/benefits/' it works. Weird, huh? In addition, both '/api/benefits' or '/api/benefits/' with GET method works...

I had a terrible hard time with this trouble and opened a question in stack overflow:

http://stackoverflow.com/questions/37834923/symfony3-routing-in-production

And then I started playing with Symfony code. Suprise! In my generated appProdUrlMatcher.php I found the following. For the first (GET) route, I could see:

if (rtrim($pathinfo, '/') === '/api/entities') {
    if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
        //bla bla
    }
}

Perfect, that's why GET works! It's "rtrimming" the route making the final slash mean nothing.

Although, with the POST one:

if ($pathinfo === '/api/entities/') {
    if ($this->context->getMethod() != 'POST') {
        //bla bla
    }
}

Wait... what? Why isn't it rtrimming my route?

Okey... Once found this, the solution was clear: go to the place where it's generated and fix it. Several minutes later I found it:

/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php::compileRoute
...
    // GET and HEAD are equivalent
    if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
        $methods[] = 'HEAD';
    }
....
    $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods));
....
    if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
        $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
        $hasTrailingSlash = true;
    } else {
        $conditions[] = sprintf('$pathinfo === %s', var_export(str_replace('\\', '', $m['url']), true));
    }
....

GET falls in the first condition, where it adds the rtrim, but POST does not.

I hope I have explained well the matter, it was not easy...

Question: Is this deliberated? If yes (I guess so), why? How can we fix then the final slash problem?

Best and many thanks,

Ignacio

Edit:

I just realized that, if the request "supports trailing slash" it also adds:

if (substr($pathinfo, -1) !== '/') {
    return $this->redirect($pathinfo.'/', 'api_list_entities');
}

Which actually breaks in POST, because the redirect is always GET, I guess that's why the condition of GET or HEAD...

How to fix this? I have a patch for the PhpMatcherDumper.php::compileRoute function, but I guess that's not the way to proceed.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions