fix(dispatcher): surface preg_match() engine failures instead of treating them as no-match

All dispatcher strategies checked preg_match() results with `!== 1`,
which conflates a genuine no-match (0) with a PCRE engine failure
(false), e.g. PREG_BACKTRACK_LIMIT_ERROR from a catastrophically
backtracking route pattern. Because dynamic routes for the same
method are combined into shared regex chunks, one such route could
make the preg_match() call for the whole chunk fail, silently
turning an unrelated, cheap route in that chunk into a spurious
NOT_FOUND.

Add a shared matchRoute() helper on RegexBasedAbstract that calls
preg_match() and throws a RuntimeException (including
preg_last_error_msg()) when it returns false, while preserving the
existing 0/1 no-match/match behavior. Update GroupCountBased,
GroupPosBased, CharCountBased and MarkBased to use it.

Fixes #167
This commit is contained in:
dualfroz committed 2026-09-04 05:29:02 +02:00
1 parent 5861a25d71
commit dea7d75d42
6 files changed
+73 -9

No files matched your search

+32
View File
@@ -8,6 +8,11 @@ use FastRoute\Dispatcher;
use FastRoute\Dispatcher\Result\Matched;
use FastRoute\Dispatcher\Result\MethodNotAllowed;
use FastRoute\Dispatcher\Result\NotMatched;
use RuntimeException;
use function preg_last_error_msg;
use function preg_match;
use function sprintf;
/**
* @internal
@@ -35,6 +40,33 @@ abstract class RegexBasedAbstract implements Dispatcher
/** @param DynamicRouteChunks $routeData */
abstract protected function dispatchVariableRoute(array $routeData, string $uri): ?Matched;
/**
* Matches a compiled route regex against the subject, distinguishing a
* genuine "no match" (preg_match() returning 0) from a PCRE engine
* failure (preg_match() returning false).
*
* A regex engine failure - for example PREG_BACKTRACK_LIMIT_ERROR caused
* by a catastrophically backtracking route pattern - must not be treated
* as "no match". Silently swallowing it can make an unrelated, cheap
* route that happens to share the same combined regex chunk spuriously
* fail to match.
*
* @see https://github.com/nikic/FastRoute/issues/167
*
* @param array<int|string, mixed>|null $matches
*/
protected function matchRoute(string $regex, string $subject, ?array &$matches = null): int
{
$result = preg_match($regex, $subject, $matches);
if ($result === false) {
throw new RuntimeException(
sprintf('Regex matching failed for "%s": %s', $regex, preg_last_error_msg()),
);
}
return $result;
}
public function dispatch(string $httpMethod, string $uri): Matched|NotMatched|MethodNotAllowed
{
if (isset($this->staticRouteMap[$httpMethod][$uri])) {