<p dir="auto">Hey,</p>
<p dir="auto">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:</p>
<p dir="auto">benefits_routing.yml<br>
entities_routing.yml<br>
user_routing.yml</p>
<p dir="auto">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:</p>
<h5 dir="auto">benefits_routing.yml</h5>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="api_list_benefits:
    path: /
    defaults: {_controller: &quot;AppBundle:Benefits:list&quot; }
    methods: [GET]
api_new_benefits:
    path: /
    defaults: {_controller: &quot;AppBundle:Benefits:new&quot; }
    methods: [POST]"><pre class="notranslate"><code class="notranslate">api_list_benefits:
    path: /
    defaults: {_controller: "AppBundle:Benefits:list" }
    methods: [GET]
api_new_benefits:
    path: /
    defaults: {_controller: "AppBundle:Benefits:new" }
    methods: [POST]
</code></pre></div>
<p dir="auto">Then, if I make a POST request to <a href="http://domain/api/benefits" rel="nofollow">http://domain/api/benefits</a>, the router should match to the second route, right? Nope.</p>
<p dir="auto">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...</p>
<p dir="auto">I had a terrible hard time with this trouble and opened a question in stack overflow:</p>
<p dir="auto"><a href="http://stackoverflow.com/questions/37834923/symfony3-routing-in-production" rel="nofollow">http://stackoverflow.com/questions/37834923/symfony3-routing-in-production</a></p>
<p dir="auto">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:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="if (rtrim($pathinfo, '/') === '/api/entities') {
    if (!in_array($this-&gt;context-&gt;getMethod(), array('GET', 'HEAD'))) {
        //bla bla
    }
}"><pre class="notranslate"><code class="notranslate">if (rtrim($pathinfo, '/') === '/api/entities') {
    if (!in_array($this-&gt;context-&gt;getMethod(), array('GET', 'HEAD'))) {
        //bla bla
    }
}
</code></pre></div>
<p dir="auto">Perfect, that's why GET works! It's "rtrimming" the route making the final slash mean nothing.</p>
<p dir="auto">Although, with the POST one:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="if ($pathinfo === '/api/entities/') {
    if ($this-&gt;context-&gt;getMethod() != 'POST') {
        //bla bla
    }
}"><pre class="notranslate"><code class="notranslate">if ($pathinfo === '/api/entities/') {
    if ($this-&gt;context-&gt;getMethod() != 'POST') {
        //bla bla
    }
}
</code></pre></div>
<p dir="auto">Wait... what? Why isn't it rtrimming my route?</p>
<p dir="auto">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:</p>
<h5 dir="auto">/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php::compileRoute</h5>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="...
    // GET and HEAD are equivalent
    if (in_array('GET', $methods) &amp;&amp; !in_array('HEAD', $methods)) {
        $methods[] = 'HEAD';
    }
....
    $supportsTrailingSlash = $supportsRedirections &amp;&amp; (!$methods || in_array('HEAD', $methods));
....
    if ($supportsTrailingSlash &amp;&amp; substr($m['url'], -1) === '/') {
        $conditions[] = sprintf(&quot;rtrim(\$pathinfo, '/') === %s&quot;, var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
        $hasTrailingSlash = true;
    } else {
        $conditions[] = sprintf('$pathinfo === %s', var_export(str_replace('\\', '', $m['url']), true));
    }
...."><pre class="notranslate"><code class="notranslate">...
    // GET and HEAD are equivalent
    if (in_array('GET', $methods) &amp;&amp; !in_array('HEAD', $methods)) {
        $methods[] = 'HEAD';
    }
....
    $supportsTrailingSlash = $supportsRedirections &amp;&amp; (!$methods || in_array('HEAD', $methods));
....
    if ($supportsTrailingSlash &amp;&amp; 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));
    }
....
</code></pre></div>
<p dir="auto">GET falls in the first condition, where it adds the rtrim, but POST does not.</p>
<p dir="auto">I hope I have explained well the matter, it was not easy...</p>
<p dir="auto">Question: Is this deliberated? If yes (I guess so), why? How can we fix then the final slash problem?</p>
<p dir="auto">Best and many thanks,</p>
<p dir="auto">Ignacio</p>
<h6 dir="auto">Edit:</h6>
<p dir="auto">I just realized that, if the request "supports trailing slash" it also adds:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="if (substr($pathinfo, -1) !== '/') {
    return $this-&gt;redirect($pathinfo.'/', 'api_list_entities');
}"><pre class="notranslate"><code class="notranslate">if (substr($pathinfo, -1) !== '/') {
    return $this-&gt;redirect($pathinfo.'/', 'api_list_entities');
}
</code></pre></div>
<p dir="auto">Which actually breaks in POST, because the redirect is always GET, I guess that's why the condition of GET or HEAD...</p>
<p dir="auto">How to fix this? I have a patch for the PhpMatcherDumper.php::compileRoute function, but I guess that's not the way to proceed.</p>
<p dir="auto">Thanks!</p>
<p dir="auto">There is currently a known limitation when using route imports that makes it impossible to to have a root route <code class="notranslate">/</code> be imported under a prefix without a trailing slash. See <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="4639548" data-permission-text="Title is private" data-url="https://github.com/symfony/symfony/issues/4322" data-hovercard-type="issue" data-hovercard-url="/symfony/symfony/issues/4322/hovercard" href="https://github.com/symfony/symfony/issues/4322">#4322</a> and <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="1409350" data-permission-text="Title is private" data-url="https://github.com/silexphp/Silex/issues/149" data-hovercard-type="issue" data-hovercard-url="/silexphp/Silex/issues/149/hovercard" href="https://github.com/silexphp/Silex/issues/149">silexphp/Silex#149</a></p>
<p dir="auto">I propose to solve this problem in a generic way by offering an option when importing routes to remove or add trailing slashes (which defaults to null, meaning not modifying anything):</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="AcmeBusinessBundle_client:
    resource: &quot;@AcmeBusinessBundle/Resources/config/routing/client.yml&quot;
    prefix:   /clients
    trailing_slashes: true|false|null"><pre class="notranslate"><code class="notranslate">AcmeBusinessBundle_client:
    resource: "@AcmeBusinessBundle/Resources/config/routing/client.yml"
    prefix:   /clients
    trailing_slashes: true|false|null
</code></pre></div>
<p dir="auto">This way, the users can unify the path on import the way they want.</p>
<p dir="auto">So for example</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="acme_business_client_list:
    path:  /
    defaults: { _controller: AcmeBusinessBundle:Client:list }"><pre class="notranslate"><code class="notranslate">acme_business_client_list:
    path:  /
    defaults: { _controller: AcmeBusinessBundle:Client:list }
</code></pre></div>
<p dir="auto">imported with <code class="notranslate">trailing_slashes: false</code> under <code class="notranslate">prefix: /clients</code> will result in path <code class="notranslate">/clients</code> instead of <code class="notranslate">/clients/</code>.</p>
<p dir="auto">The other case with <code class="notranslate">trailing_slashes: true</code> is helpful when you import third party routes without trailing slash but you decided that you app should use training slashes everywhere consistently.</p>