Skip to content

Commit 77bdf79

Browse files
committed
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).
1 parent 79d83fe commit 77bdf79

5 files changed

Lines changed: 133 additions & 25 deletions

File tree

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,8 @@ svr.set_post_routing_handler([](const auto& req, auto& res) {
450450

451451
### Pre request handler
452452

453+
The pre-request handler runs after the route has been matched (so `req.matched_route` and `req.path_params` are available) but **before the request body is read**. This means you can reject a request — for example on a failed authentication or authorization check — without forcing the server to buffer a potentially large body.
454+
453455
```cpp
454456
svr.set_pre_request_handler([](const auto& req, auto& res) {
455457
if (req.matched_route == "/user/:user") {
@@ -464,6 +466,38 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
464466
});
465467
```
466468

469+
> [!NOTE]
470+
> Because the body has not been read yet, `req.body` and form fields parsed from the body are not available in the pre-request handler. Inspect headers, the path, query parameters, or `req.matched_route` instead.
471+
472+
### Handler execution order
473+
474+
`set_start_handler` runs once when the server starts. For each request, handlers run in the following order:
475+
476+
```
477+
Request received
478+
479+
├─ pre_routing_handler route not matched yet, body not read
480+
│ └─ returns Handled → stop here
481+
482+
├─ file_request_handler (GET/HEAD, static file serving)
483+
484+
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
485+
486+
├─ route matching → req.matched_route is set
487+
488+
├─ pre_request_handler route matched, body NOT read yet
489+
│ └─ returns Handled → stop here (route handler is skipped)
490+
491+
├─ route handler Get/Post/...; the request body is read first
492+
493+
└─ post_routing_handler after routing completes
494+
495+
On a thrown exception → exception_handler
496+
On an error status (4xx/5xx) → error_handler
497+
```
498+
499+
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
500+
467501
### Response user data
468502

469503
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.

docs-src/pages/en/cookbook/s11-pre-request.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ The `set_pre_routing_handler()` from [S09. Add pre-processing to all routes](s09
88

99
## Pre-routing vs. pre-request
1010

11-
| Hook | When it runs | Route info |
12-
| --- | --- | --- |
13-
| `set_pre_routing_handler` | Before routing | Not available |
14-
| `set_pre_request_handler` | After routing, right before the route handler | Available via `req.matched_route` |
11+
| Hook | When it runs | Route info | Request body |
12+
| --- | --- | --- | --- |
13+
| `set_pre_routing_handler` | Before routing | Not available | Not read yet |
14+
| `set_pre_request_handler` | After routing, right before the route handler | Available via `req.matched_route` | Not read yet |
1515

1616
In a pre-request handler, `req.matched_route` holds the **pattern string** that matched. You can vary behavior based on the route definition itself.
1717

18+
Because the body has not been read when the pre-request handler runs, you can reject a request — for example on a failed auth check — without consuming a (potentially large) request body. Note that this also means `req.body` and form fields parsed from the body are not available here; inspect headers, the path, query parameters, or `req.matched_route` instead.
19+
1820
## Switch auth per route
1921

2022
```cpp

docs-src/pages/ja/cookbook/s11-pre-request.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ status: "draft"
88

99
## Pre-routingとの違い
1010

11-
| フック | 呼ばれるタイミング | ルート情報 |
12-
| --- | --- | --- |
13-
| `set_pre_routing_handler` | ルーティングの前 | 取得できない |
14-
| `set_pre_request_handler` | ルーティング後、ルートハンドラの直前 | `req.matched_route`で取得可能 |
11+
| フック | 呼ばれるタイミング | ルート情報 | リクエストボディ |
12+
| --- | --- | --- | --- |
13+
| `set_pre_routing_handler` | ルーティングの前 | 取得できない | まだ読まれていない |
14+
| `set_pre_request_handler` | ルーティング後、ルートハンドラの直前 | `req.matched_route`で取得可能 | まだ読まれていない |
1515

1616
Pre-requestハンドラなら、`req.matched_route`に「マッチしたパターン文字列」が入っているので、ルートに応じて処理を変えられます。
1717

18+
Pre-requestハンドラが呼ばれる時点ではボディがまだ読まれていないので、認証に失敗したリクエストなどを、(巨大かもしれない)ボディを読み込む前に拒否できます。その代わり、`req.body`やボディから解析されるフォームフィールドはこの時点では参照できません。ヘッダ・パス・クエリパラメータ・`req.matched_route`を使って判断してください。
19+
1820
## ルートごとに認証を切り替える
1921

2022
```cpp

httplib.h

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,8 +1812,8 @@ class Server {
18121812
const std::string &etag, time_t mtime) const;
18131813
bool check_if_range(Request &req, const std::string &etag,
18141814
time_t mtime) const;
1815-
bool dispatch_request(Request &req, Response &res,
1816-
const Handlers &handlers) const;
1815+
bool dispatch_request(Request &req, Response &res, const Handlers &handlers,
1816+
Stream &strm);
18171817
bool dispatch_request_for_content_reader(
18181818
Request &req, Response &res, ContentReader content_reader,
18191819
const HandlersForContentReader &handlers) const;
@@ -11955,44 +11955,56 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
1195511955
}
1195611956
}
1195711957

11958-
// Read content into `req.body`
11959-
if (!read_content(strm, req, res)) {
11960-
output_error_log(Error::Read, &req);
11961-
return false;
11962-
}
11958+
// NOTE: `req.body` is not read here. For a regular handler the body is
11959+
// read inside dispatch_request(), after the route has matched and the
11960+
// pre-request handler has approved the request, so that a rejected
11961+
// request (e.g. failed authentication) never forces us to buffer a
11962+
// potentially large body.
1196311963
}
1196411964

1196511965
// Regular handler
1196611966
if (req.method == "GET" || req.method == "HEAD") {
11967-
return dispatch_request(req, res, get_handlers_);
11967+
return dispatch_request(req, res, get_handlers_, strm);
1196811968
} else if (req.method == "POST") {
11969-
return dispatch_request(req, res, post_handlers_);
11969+
return dispatch_request(req, res, post_handlers_, strm);
1197011970
} else if (req.method == "PUT") {
11971-
return dispatch_request(req, res, put_handlers_);
11971+
return dispatch_request(req, res, put_handlers_, strm);
1197211972
} else if (req.method == "DELETE") {
11973-
return dispatch_request(req, res, delete_handlers_);
11973+
return dispatch_request(req, res, delete_handlers_, strm);
1197411974
} else if (req.method == "OPTIONS") {
11975-
return dispatch_request(req, res, options_handlers_);
11975+
return dispatch_request(req, res, options_handlers_, strm);
1197611976
} else if (req.method == "PATCH") {
11977-
return dispatch_request(req, res, patch_handlers_);
11977+
return dispatch_request(req, res, patch_handlers_, strm);
1197811978
}
1197911979

1198011980
res.status = StatusCode::BadRequest_400;
1198111981
return false;
1198211982
}
1198311983

1198411984
inline bool Server::dispatch_request(Request &req, Response &res,
11985-
const Handlers &handlers) const {
11985+
const Handlers &handlers, Stream &strm) {
1198611986
for (const auto &x : handlers) {
1198711987
const auto &matcher = x.first;
1198811988
const auto &handler = x.second;
1198911989

1199011990
if (matcher->match(req)) {
1199111991
req.matched_route = matcher->pattern();
11992-
if (!pre_request_handler_ ||
11993-
pre_request_handler_(req, res) != HandlerResponse::Handled) {
11994-
handler(req, res);
11992+
11993+
// Run the pre-request handler before reading the body so a rejected
11994+
// request (e.g. failed authentication) never forces us to buffer a
11995+
// potentially large body. `req.matched_route` is available here.
11996+
if (pre_request_handler_ &&
11997+
pre_request_handler_(req, res) == HandlerResponse::Handled) {
11998+
return true;
1199511999
}
12000+
12001+
// The route matched and the request was approved; read the body now.
12002+
if (detail::expect_content(req) && !read_content(strm, req, res)) {
12003+
output_error_log(Error::Read, &req);
12004+
return false;
12005+
}
12006+
12007+
handler(req, res);
1199612008
return true;
1199712009
}
1199812010
}

test/test.cc

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3289,6 +3289,64 @@ TEST(RequestHandlerTest, PreRequestHandler) {
32893289
}
32903290
}
32913291

3292+
// The pre-request handler must run before the request body is read, so a
3293+
// rejected request never forces the server to buffer a (potentially large)
3294+
// body. Here the posted body is larger than the payload limit: if the body
3295+
// were read first the server would answer 413, but because the pre-request
3296+
// handler runs first it answers 403 and the body is never read.
3297+
TEST(RequestHandlerTest, PreRequestHandlerRunsBeforeBodyIsRead) {
3298+
Server svr;
3299+
3300+
svr.set_payload_max_length(8);
3301+
3302+
auto handler_ran = false;
3303+
svr.Post("/reject", [&](const Request &req, Response &res) {
3304+
handler_ran = true;
3305+
res.set_content(req.body, "text/plain");
3306+
});
3307+
3308+
svr.Post("/accept", [](const Request &req, Response &res) {
3309+
res.set_content(req.body, "text/plain");
3310+
});
3311+
3312+
svr.set_pre_request_handler([](const Request &req, Response &res) {
3313+
if (req.matched_route == "/reject") {
3314+
res.status = StatusCode::Forbidden_403;
3315+
res.set_content("denied", "text/plain");
3316+
return Server::HandlerResponse::Handled;
3317+
}
3318+
return Server::HandlerResponse::Unhandled;
3319+
});
3320+
3321+
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
3322+
auto se = detail::scope_exit([&] {
3323+
svr.stop();
3324+
thread.join();
3325+
ASSERT_FALSE(svr.is_running());
3326+
});
3327+
3328+
svr.wait_until_ready();
3329+
3330+
Client cli(HOST, PORT);
3331+
3332+
// Body (10 bytes) exceeds the 8-byte limit, yet the pre-request handler
3333+
// rejects with 403 before the body is read, so 413 is never reached.
3334+
{
3335+
auto res = cli.Post("/reject", "0123456789", "text/plain");
3336+
ASSERT_TRUE(res);
3337+
EXPECT_EQ(StatusCode::Forbidden_403, res->status);
3338+
EXPECT_EQ("denied", res->body);
3339+
EXPECT_FALSE(handler_ran);
3340+
}
3341+
3342+
// An approved route still reads the body and enforces the payload limit.
3343+
{
3344+
auto res = cli.Post("/accept", "0123456789", "text/plain");
3345+
ASSERT_TRUE(res);
3346+
EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status);
3347+
}
3348+
}
3349+
32923350
TEST(UserDataTest, BasicOperations) {
32933351
httplib::UserData ud;
32943352

0 commit comments

Comments
 (0)