Match literal route patterns without building a std::regex (#2538)

Server::make_matcher() built a std::regex for every pattern that did not
contain "/:", even though most route patterns are plain literals with no
regular expression syntax in them. Matching those went through
std::regex_match on every request, for every registered route the
dispatcher scanned before reaching the one that matches.

PathParamsMatcher already performs an exact literal comparison when it
captures no parameter, so no new matcher class is needed: a pattern with
no regex metacharacter can simply use it. Add an early return for the
zero parameter case in PathParamsMatcher::match(), and select the matcher
by also looking for the 14 ECMAScript metacharacters instead of only for
"/:". Path params keep taking precedence, so a pattern that mixes both,
such as "/users/:id/(.*)", is unaffected.

Measured with clang -O2 on macOS, scanning routes that all miss until the
last one: at 100 routes a scan drops from 10.3us to 0.46us, and end to end
throughput rises by about 24%. At 1000 routes throughput is roughly 3
times higher. Registering 5000 routes drops from about 3.0ms to about
0.9ms, since no std::regex is built for literal patterns.

This also keeps CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH confined to the
routes it is meant for. That limit rejects overlong paths before calling
std::regex_match, but until now every literal route was a RegexMatcher
too, so a literal route longer than the limit stopped matching even though
no regular expression was involved. Literal routes no longer go through
RegexMatcher, so only real regex routes are capped.

Patterns containing a metacharacter keep their current behavior, so
"/index.html" still matches "/indexXhtml" the way it always has. One
visible change: a literal route no longer populates Request::matches,
which is now a default constructed std::smatch. Path parameter routes
have always behaved that way, and Request::matches only carries useful
information for regex routes.
This commit is contained in:
yhirose
2026-08-14 12:58:01 -04:00
committed by GitHub
parent 88240172ea
commit 1e9d6f0b0b
2 changed files with 89 additions and 2 deletions
+73
View File
@@ -15461,6 +15461,79 @@ TEST(RegexMatcherTest, ServerSurvivesOverlongPathOnRegexRoute) {
EXPECT_NE(std::string::npos, response.find("404"));
}
TEST(RouteMatcherTest, LiteralPatternsAndRegexPatterns) {
Server svr;
auto handler = [](const Request & /*req*/, Response &res) {
res.set_content("hit", "text/plain");
};
// No regex metacharacter, so this one is compared literally
svr.Get("/literal/route", handler);
// '.' is a regex metacharacter, so this one must keep regex semantics
svr.Get("/a.c", handler);
auto port = svr.bind_to_any_port(HOST);
std::thread t([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
struct {
const char *path;
int status;
} cases[] = {
{"/literal/route", StatusCode::OK_200},
{"/literal/routes", StatusCode::NotFound_404},
{"/literal/rout", StatusCode::NotFound_404},
{"/abc", StatusCode::OK_200},
{"/a.c", StatusCode::OK_200},
};
for (const auto &x : cases) {
auto res = cli.Get(x.path);
ASSERT_TRUE(res) << x.path;
EXPECT_EQ(x.status, res->status) << x.path;
}
}
TEST(RouteMatcherTest, LongLiteralPatternIsNotSubjectToTheRegexPathLimit) {
// CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH exists to keep std::regex_match
// from exhausting the stack, so it must only constrain routes that actually
// run a regular expression. A literal route is matched by comparison and
// stays usable at any length.
Server svr;
const std::string pattern =
"/files/" + std::string(CPPHTTPLIB_REGEX_ROUTE_PATH_MAX_LENGTH * 2, 'a');
svr.Get(pattern, [](const Request & /*req*/, Response &res) {
res.set_content("hit", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
std::thread t([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
auto res = cli.Get(pattern);
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
}
TEST(ParseUrlTest, VariousPatterns) {
{
detail::UrlComponents uc;