Description
Symfony version(s) affected: 4.2.4
Description
We have a few sets of urls that have the same path for distinct hosts.
In one of those sets, the paths also differ in whether a trailing slash is required or not.
The expected behavior is that each of those urls properly matches the combination of host+path. In reality the too eager "do I understand redirects for this non-trailing-slash url?" effectively prevents the matches from going through.
The actual behavior is a 404.
How to reproduce
I've dumbed down the routes a bit (and converted it from php-routes to yaml), but this should still create the issue:
reviews.portal:
path: /reviews/
host: tweakers.net
controller: Tweakers\ReviewController::reviewsAction
cms.reviews.portal:
path: /reviews/
host: cms.tweakers.net
controller: Tweakers\CmsReviewController::reviewsAction
othercms.reviews.portal:
path: /reviews # Without trailing /
host: othercms.tweakers.net
controller: Tweakers\OtherCmsReviewController::reviewsAction
The generated static routes tabel in the cached url matcher php-file becomes this:
'/reviews' => [
[['_route' => 'reviews.portal', '_controller' => 'Tweakers\\ReviewController::reviewsAction'], 'tweakers.net', null, null, true, false, null],
[['_route' => 'cms.reviews.portal', '_controller' => 'Tweakers\\CmsReviewController::reviewsAction'], 'cms.tweakers.net', null, null, true, false, null],
[['_route' => 'othercms.reviews.portal', '_controller' => 'Tweakers\\OtherCmsController::reviewsAction'], 'othercms.tweakers.net', null, null, false, false, null],
],
The only difference between those routes, other than the host name, is the 'false' for 'hasTrailingSlash'.
This data is used in the first foreach in PhpMatcherTrait::doMatch
When a route for 'https://othercms.tweakers.net/reviews' (without trailing slash) is requested/tested, it is first matched against route 'reviews.portal' for 'tweakers.net'. The redirect-test in the foreach recognizes it can support /reviews/ (with trailing slash) and does a early return.
The PhpMatcherTrait::match()-method than retries the matching basically with 'https://othercms.tweakers.net/reviews/' to see if it can really create a redirect. This is tested against all three available routes for '/reviews' and this time (correctly) does not match 'reviews.portal' since the requested host does not match that route.
None of the other routes matches (but they would cause unexpected behavior anyway with the altered url). The end result is a 404.
Note the same code would also fail with similar differences in the scheme and possibly some variants of methods (if you do a get/head-request on a post-route).
Btw, I'm also not convinced its a good idea to adjust the $allowSchemes in that method. It seems to create similar issues in the match()-method, since it could issue a scheme-redirect for any host that may result in no eventual matches against the route (which could result in a redirect-loop).
Possible Solution
Since the code is rather complex, I don't like making a pull request for it. But my understanding of the method suggests the test for redirects should be moved to after the host and scheme tests. It should first fully decide the route is a actual match and only than decide to redirect.
A more robust, but more complex, solution would be to adjust the created staticRoutes-cache, to something like this structure:
['hostA' => ['/path' => [...]],
'hostB' => ['/path' => [...]],
'' => [...], // Or some other way to signal routes for generic hosts
But that may be a bit tricky with the wildcard support. It would allow a potential performance benefit by removing the host-test from the foreach-loop, since that can be done by selecting the proper cache.
The regexp-based variant seems to be unaffected by hostnames (those are inlined in the regexp), but also tests redirects first and then tests the scheme. So that needs some consideration as well.