Read request body after route matching and pre-request handler

Previously, for regular handlers the request body was read in routing()
before the route was matched, so the pre-request handler always saw an
already-read body. The ContentReader path, in contrast, ran the
pre-request handler before the body was read. This inconsistency made
it impossible to reject a request (e.g. failed per-route authentication
via req.matched_route) without buffering a potentially large body.

Move the read_content() call into dispatch_request(), after route
matching and the pre-request handler, so both paths behave the same:
route matching -> pre_request_handler -> body read -> handler. A
request rejected by the pre-request handler no longer reads the body
at all; the existing keep-alive drain logic still consumes any framed
body afterwards.

Note: code that referenced req.body or body-derived form fields inside
the pre-request handler will now see an empty body. Inspect headers,
path, query parameters, or matched_route instead.

Also document the handler execution order in README and update the
pre-request cookbook pages (en/ja).
This commit is contained in:
yhirose
2026-06-09 13:49:56 -04:00
parent 79d83feb18
commit 77bdf7921a
5 changed files with 133 additions and 25 deletions
+58
View File
@@ -3289,6 +3289,64 @@ TEST(RequestHandlerTest, PreRequestHandler) {
}
}
// The pre-request handler must run before the request body is read, so a
// rejected request never forces the server to buffer a (potentially large)
// body. Here the posted body is larger than the payload limit: if the body
// were read first the server would answer 413, but because the pre-request
// handler runs first it answers 403 and the body is never read.
TEST(RequestHandlerTest, PreRequestHandlerRunsBeforeBodyIsRead) {
Server svr;
svr.set_payload_max_length(8);
auto handler_ran = false;
svr.Post("/reject", [&](const Request &req, Response &res) {
handler_ran = true;
res.set_content(req.body, "text/plain");
});
svr.Post("/accept", [](const Request &req, Response &res) {
res.set_content(req.body, "text/plain");
});
svr.set_pre_request_handler([](const Request &req, Response &res) {
if (req.matched_route == "/reject") {
res.status = StatusCode::Forbidden_403;
res.set_content("denied", "text/plain");
return Server::HandlerResponse::Handled;
}
return Server::HandlerResponse::Unhandled;
});
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
// Body (10 bytes) exceeds the 8-byte limit, yet the pre-request handler
// rejects with 403 before the body is read, so 413 is never reached.
{
auto res = cli.Post("/reject", "0123456789", "text/plain");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::Forbidden_403, res->status);
EXPECT_EQ("denied", res->body);
EXPECT_FALSE(handler_ran);
}
// An approved route still reads the body and enforces the payload limit.
{
auto res = cli.Post("/accept", "0123456789", "text/plain");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status);
}
}
TEST(UserDataTest, BasicOperations) {
httplib::UserData ud;