Description
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!