diff --git a/README.md b/README.md index d4bbb2e..bc9c8fe 100644 --- a/README.md +++ b/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. diff --git a/docs-src/pages/en/cookbook/s11-pre-request.md b/docs-src/pages/en/cookbook/s11-pre-request.md index 303b359..a1422a9 100644 --- a/docs-src/pages/en/cookbook/s11-pre-request.md +++ b/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 diff --git a/docs-src/pages/ja/cookbook/s11-pre-request.md b/docs-src/pages/ja/cookbook/s11-pre-request.md index 9532200..a075fd1 100644 --- a/docs-src/pages/ja/cookbook/s11-pre-request.md +++ b/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 diff --git a/httplib.h b/httplib.h index 4fc027d..1b9a114 100644 --- a/httplib.h +++ b/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; } } diff --git a/test/test.cc b/test/test.cc index 949edfe..70cffc3 100644 --- a/test/test.cc +++ b/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;