-
Notifications
You must be signed in to change notification settings - Fork 448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
POST route + custom placeholder pattern is being overridden by GET route + default placeholder pattern #25
Comments
I created some more test cases to better illustrate what is going on: <?php
require_once './vendor/autoload.php';
function testRoute($dispatcher, $method, $path) {
echo " $method $path:", "\n ";
$routeInfo = $dispatcher->dispatch($method, $path);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
echo 'NOT FOUND';
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = implode(', ', $routeInfo[1]);
echo "METHOD NOT ALLOWED (Allowed methods are $allowedMethods)";
break;
case FastRoute\Dispatcher::FOUND:
echo "matched '{$routeInfo[1]}'";
break;
}
echo "\n\n";
}
echo <<<EOT
*** Test #1: expected behavior
*** POST route with default placeholder pattern correctly matches \n\n
EOT;
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'GET with default placeholder pattern');
$r->addRoute('POST', '/user/{name}', 'POST with default placeholder pattern');
});
testRoute($dispatcher, 'POST', '/user/fred');
echo <<<EOT
*** Test #2: unexpected behavior
*** GET route incorrectly takes priority over the POST route with custom
placeholder pattern \n\n
EOT;
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'GET with default placeholder pattern');
$r->addRoute('POST', '/user/{name:[a-z]+}', 'POST with custom placeholder pattern');
});
testRoute($dispatcher, 'POST', '/user/fred');
echo <<<EOT
*** Test #3: unexpected behavior
*** POST route with custom placeholder pattern incorrectly takes priority over
the GET route \n\n
EOT;
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('POST', '/user/{name:[a-z]+}', 'POST with custom placeholder pattern');
$r->addRoute('GET', '/user/{name}', 'GET with default placeholder pattern');
});
testRoute($dispatcher, 'GET', '/user/bob');
echo <<<EOT
*** Test #4: unexpected behavior
*** POST route with custom placeholder pattern route is incorrectly ignored when
GET comes before it, instead matching POST with default placeholder pattern \n\n
EOT;
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/user/{name}', 'GET with default placeholder pattern');
$r->addRoute('POST', '/user/{name:[a-z]+}', 'POST with custom placeholder pattern');
$r->addRoute('POST', '/user/{name}', 'POST with default placeholder pattern');
});
testRoute($dispatcher, 'POST', '/user/fred');
echo <<<EOT
*** Test #5: expected behavior
*** POST with custom placeholder pattern route matches when GET comes after it \n\n
EOT;
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('POST', '/user/{name:[a-z]+}', 'POST with custom placeholder pattern');
$r->addRoute('GET', '/user/{name}', 'GET with default placeholder pattern');
});
testRoute($dispatcher, 'POST', '/user/fred'); Output:
|
Wow, this looks like a pretty serious issue. Currently the router first finds a matching route and then checks if it is allowed for this method. As your example shows, this doesn't work correctly if there are two routes for different methods, where one matches a subset of the other. So, just a brain dump on how we might fix this: The first approach would be to not stop at the first matched route if the method is not allowed - instead remember the allowed methods and continue doing further matches. If we find a route that matches and is allowed later on, we use it. Otherwise return At which point another question comes up: What is the "allowed method list" supposed to look like in this case? Currently it's the allowed methods for the first matched route, but probably it should be the union of the allowed methods of all matching routes. But I don't think it's actually possible to implement this approach - the way routes are matched is designed to always provide the first matching route, skipping a route during matching would probably be rather complicated and/or slow. The alternative is to invert the matching order: Have separate route maps for different methods and only match the routes for the given method (with HEAD fallback as usual). If a route matches, it will be the right one for this method. However it is less clear what should be done if there is no match. To distinguish between a "method not allowed" and a "not found" and to determine the allowed methods list would, at least naively, require matching against the routes of all other methods as well, which does not sound terribly efficient. On the other hand, the performance of the error case usually isn't the important bit, so maybe that's quite okay. |
I'm still trying to get a better understanding of the inner workings of FastRoute, but you mentioned METHOD_NOT_ALLOWED possibly playing a role. I always wondered what the purpose of this value being returned is, because as an end user of the library, aren't we only concerned with: "did the route match? yes or no"? To me, a matching path but non-matching method would simply be considered a NOT_FOUND, and I think that's also the convention of other popular routers such as the one in Rails. What is the use case for having METHOD_NOT_ALLOWED (aside from providing a more informative error message) and if it was removed, would that help to solve this issue? |
@msheakoski The |
Regarding my question on the usefulness of |
I can confirm this issue. |
Instead of first matching against the route and then checking the allowed methods, the dispatch now happens in reverse: Only the routes for a particular method are considered initially. If there is no route for the specified method, the URI will be matched against all other methods as well in order to determine the allowed methods list (or conclude that no route exists at all). The performance impact of this change is twofold: On the one hand fewer matches are required in case a route is found (because only the method we're actually interested is tested). On the other hand there is more logic and potentially also more matches necessary in cases where a method-not-allowed or not-found status occurs.
Should be fixed by 5b16723. |
Thank you, @nikic! This looks great! Do you plan on bumping the version as well for composer users? |
In the code below, the GET route is overriding the POST route whenever I use a custom pattern. However, if I try the same POST route with the default pattern, it does correctly match.
The text was updated successfully, but these errors were encountered: