Kaynağa Gözat

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).
yhirose 4 ay önce
ebeveyn
işleme
77bdf7921a

+ 34 - 0
README.md

@@ -450,6 +450,8 @@ svr.set_post_routing_handler([](const auto& req, auto& res) {
 
 ### Pre request handler
 
+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.
+
 ```cpp
 svr.set_pre_request_handler([](const auto& req, auto& res) {
   if (req.matched_route == "/user/:user") {
@@ -464,6 +466,38 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
 });
 ```
 
+> [!NOTE]
+> 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.
+
+### Handler execution order
+
+`set_start_handler` runs once when the server starts. For each request, handlers run in the following order:
+
+```
+Request received
+  │
+  ├─ pre_routing_handler          route not matched yet, body not read
+  │     └─ returns Handled → stop here
+  │
+  ├─ file_request_handler         (GET/HEAD, static file serving)
+  │
+  ├─ expect_100_continue_handler  (when the request has "Expect: 100-continue")
+  │
+  ├─ route matching → req.matched_route is set
+  │
+  ├─ pre_request_handler          route matched, body NOT read yet
+  │     └─ returns Handled → stop here (route handler is skipped)
+  │
+  ├─ route handler                Get/Post/...; the request body is read first
+  │
+  └─ post_routing_handler         after routing completes
+
+  On a thrown exception → exception_handler
+  On an error status (4xx/5xx) → error_handler
+```
+
+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.
+
 ### Response user data
 
 `res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.

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

@@ -8,13 +8,15 @@ The `set_pre_routing_handler()` from [S09. Add pre-processing to all routes](s09
 
 ## Pre-routing vs. pre-request
 
-| Hook | When it runs | Route info |
-| --- | --- | --- |
-| `set_pre_routing_handler` | Before routing | Not available |
-| `set_pre_request_handler` | After routing, right before the route handler | Available via `req.matched_route` |
+| Hook | When it runs | Route info | Request body |
+| --- | --- | --- | --- |
+| `set_pre_routing_handler` | Before routing | Not available | Not read yet |
+| `set_pre_request_handler` | After routing, right before the route handler | Available via `req.matched_route` | Not read yet |
 
 In a pre-request handler, `req.matched_route` holds the **pattern string** that matched. You can vary behavior based on the route definition itself.
 
+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.
+
 ## Switch auth per route
 
 ```cpp

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

@@ -8,13 +8,15 @@ status: "draft"
 
 ## Pre-routingとの違い
 
-| フック | 呼ばれるタイミング | ルート情報 |
-| --- | --- | --- |
-| `set_pre_routing_handler` | ルーティングの前 | 取得できない |
-| `set_pre_request_handler` | ルーティング後、ルートハンドラの直前 | `req.matched_route`で取得可能 |
+| フック | 呼ばれるタイミング | ルート情報 | リクエストボディ |
+| --- | --- | --- | --- |
+| `set_pre_routing_handler` | ルーティングの前 | 取得できない | まだ読まれていない |
+| `set_pre_request_handler` | ルーティング後、ルートハンドラの直前 | `req.matched_route`で取得可能 | まだ読まれていない |
 
 Pre-requestハンドラなら、`req.matched_route`に「マッチしたパターン文字列」が入っているので、ルートに応じて処理を変えられます。
 
+Pre-requestハンドラが呼ばれる時点ではボディがまだ読まれていないので、認証に失敗したリクエストなどを、(巨大かもしれない)ボディを読み込む前に拒否できます。その代わり、`req.body`やボディから解析されるフォームフィールドはこの時点では参照できません。ヘッダ・パス・クエリパラメータ・`req.matched_route`を使って判断してください。
+
 ## ルートごとに認証を切り替える
 
 ```cpp

+ 29 - 17
httplib.h

@@ -1812,8 +1812,8 @@ private:
                              const std::string &etag, time_t mtime) const;
   bool check_if_range(Request &req, const std::string &etag,
                       time_t mtime) const;
-  bool dispatch_request(Request &req, Response &res,
-                        const Handlers &handlers) const;
+  bool dispatch_request(Request &req, Response &res, const Handlers &handlers,
+                        Stream &strm);
   bool dispatch_request_for_content_reader(
       Request &req, Response &res, ContentReader content_reader,
       const HandlersForContentReader &handlers) const;
@@ -11955,26 +11955,26 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
       }
     }
 
-    // Read content into `req.body`
-    if (!read_content(strm, req, res)) {
-      output_error_log(Error::Read, &req);
-      return false;
-    }
+    // NOTE: `req.body` is not read here. For a regular handler the body is
+    // read inside dispatch_request(), after the route has matched and the
+    // pre-request handler has approved the request, so that a rejected
+    // request (e.g. failed authentication) never forces us to buffer a
+    // potentially large body.
   }
 
   // Regular handler
   if (req.method == "GET" || req.method == "HEAD") {
-    return dispatch_request(req, res, get_handlers_);
+    return dispatch_request(req, res, get_handlers_, strm);
   } else if (req.method == "POST") {
-    return dispatch_request(req, res, post_handlers_);
+    return dispatch_request(req, res, post_handlers_, strm);
   } else if (req.method == "PUT") {
-    return dispatch_request(req, res, put_handlers_);
+    return dispatch_request(req, res, put_handlers_, strm);
   } else if (req.method == "DELETE") {
-    return dispatch_request(req, res, delete_handlers_);
+    return dispatch_request(req, res, delete_handlers_, strm);
   } else if (req.method == "OPTIONS") {
-    return dispatch_request(req, res, options_handlers_);
+    return dispatch_request(req, res, options_handlers_, strm);
   } else if (req.method == "PATCH") {
-    return dispatch_request(req, res, patch_handlers_);
+    return dispatch_request(req, res, patch_handlers_, strm);
   }
 
   res.status = StatusCode::BadRequest_400;
@@ -11982,17 +11982,29 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
 }
 
 inline bool Server::dispatch_request(Request &req, Response &res,
-                                     const Handlers &handlers) const {
+                                     const Handlers &handlers, Stream &strm) {
   for (const auto &x : handlers) {
     const auto &matcher = x.first;
     const auto &handler = x.second;
 
     if (matcher->match(req)) {
       req.matched_route = matcher->pattern();
-      if (!pre_request_handler_ ||
-          pre_request_handler_(req, res) != HandlerResponse::Handled) {
-        handler(req, res);
+
+      // Run the pre-request handler before reading the body so a rejected
+      // request (e.g. failed authentication) never forces us to buffer a
+      // potentially large body. `req.matched_route` is available here.
+      if (pre_request_handler_ &&
+          pre_request_handler_(req, res) == HandlerResponse::Handled) {
+        return true;
       }
+
+      // The route matched and the request was approved; read the body now.
+      if (detail::expect_content(req) && !read_content(strm, req, res)) {
+        output_error_log(Error::Read, &req);
+        return false;
+      }
+
+      handler(req, res);
       return true;
     }
   }

+ 58 - 0
test/test.cc

@@ -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;