From f29721c7d8ed086afdfe68b381c72db25d8aff1f Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 21:38:40 -0400 Subject: [PATCH] Treat 0 as no limit for the remaining limits set_keep_alive_max_count(0) tripped an assert, a negative WebSocket ping interval sent pings in a tight loop, and the form length, multipart file count and range count macros rejected every request at 0. Make 0 mean no limit for those, and a ping interval of 0 or less turn pings off. The URI, header and line bounds cannot be lifted, so a static_assert keeps them positive. --- README.md | 15 +++--- docs-src/pages/en/cookbook/s20-keep-alive.md | 2 +- docs-src/pages/ja/cookbook/s20-keep-alive.md | 2 +- httplib.h | 38 ++++++++++---- test/test.cc | 55 ++++++++++++++++++++ 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 40a43236..24f9c1ed 100644 --- a/README.md +++ b/README.md @@ -764,11 +764,12 @@ svr.Post("/content_receiver", }); ``` -`CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT` (default 1024) caps the number of -form-data parts only on the buffered path, where every part is accumulated into -`req.form`. The content receiver keeps nothing, so the cap does not apply here. -If your handler needs an upper bound on the number of parts, count them yourself -and return `false` from the callback to stop the parser. +`CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT` (default 1024, `0` for no limit) +caps the number of form-data parts only on the buffered path, where every part +is accumulated into `req.form`. The content receiver keeps nothing, so the cap +does not apply here. If your handler needs an upper bound on the number of +parts, count them yourself and return `false` from the callback to stop the +parser. ### Send content with the content provider @@ -884,7 +885,7 @@ svr.set_expect_100_continue_handler([](const Request &req, Response &res) { ### Keep-Alive connection ```cpp -svr.set_keep_alive_max_count(2); // Default is 100 +svr.set_keep_alive_max_count(2); // Default is 100, 0 for no limit svr.set_keep_alive_timeout(10); // Default is 5 ``` @@ -909,7 +910,7 @@ svr.set_payload_max_length(1024 * 1024 * 512); // 512MB ``` > [!NOTE] -> When the request body content type is 'www-form-urlencoded', the actual payload length shouldn't exceed `CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH`. +> When the request body content type is 'www-form-urlencoded', the actual payload length shouldn't exceed `CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH` (`0` for no limit). ### Server-Sent Events diff --git a/docs-src/pages/en/cookbook/s20-keep-alive.md b/docs-src/pages/en/cookbook/s20-keep-alive.md index 3b9f1182..32a4db58 100644 --- a/docs-src/pages/en/cookbook/s20-keep-alive.md +++ b/docs-src/pages/en/cookbook/s20-keep-alive.md @@ -10,7 +10,7 @@ status: "draft" | API | Default | Meaning | | --- | --- | --- | -| `set_keep_alive_max_count` | 100 | Max requests served over a single connection | +| `set_keep_alive_max_count` | 100 | Max requests served over a single connection (0 for no limit) | | `set_keep_alive_timeout` | 5s | How long an idle connection is kept before closing | ## Basic usage diff --git a/docs-src/pages/ja/cookbook/s20-keep-alive.md b/docs-src/pages/ja/cookbook/s20-keep-alive.md index 47605a77..b14a87ea 100644 --- a/docs-src/pages/ja/cookbook/s20-keep-alive.md +++ b/docs-src/pages/ja/cookbook/s20-keep-alive.md @@ -10,7 +10,7 @@ status: "draft" | API | デフォルト | 意味 | | --- | --- | --- | -| `set_keep_alive_max_count` | 100 | 1本の接続で受け付けるリクエストの最大数 | +| `set_keep_alive_max_count` | 100 | 1本の接続で受け付けるリクエストの最大数(0 で無制限) | | `set_keep_alive_timeout` | 5秒 | アイドル状態の接続を閉じるまでの秒数 | ## 基本の使い方 diff --git a/httplib.h b/httplib.h index b0ccd761..73edd852 100644 --- a/httplib.h +++ b/httplib.h @@ -223,6 +223,16 @@ #define CPPHTTPLIB_MAX_LINE_LENGTH 32768 #endif +// These bound the parsing of every request, so 0 cannot lift them. +static_assert(CPPHTTPLIB_REQUEST_URI_MAX_LENGTH > 0, + "CPPHTTPLIB_REQUEST_URI_MAX_LENGTH must be positive"); +static_assert(CPPHTTPLIB_HEADER_MAX_LENGTH > 0, + "CPPHTTPLIB_HEADER_MAX_LENGTH must be positive"); +static_assert(CPPHTTPLIB_HEADER_MAX_COUNT > 0, + "CPPHTTPLIB_HEADER_MAX_COUNT must be positive"); +static_assert(CPPHTTPLIB_MAX_LINE_LENGTH > 0, + "CPPHTTPLIB_MAX_LINE_LENGTH must be positive"); + #ifndef CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH #define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216 #endif @@ -6826,20 +6836,19 @@ inline bool process_server_socket_core(const std::atomic &svr_sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, P has_buffered_request, T callback) { - assert(keep_alive_max_count > 0); auto ret = false; - auto count = keep_alive_max_count; - while (count > 0) { + // A keep_alive_max_count of 0 sets no limit. + for (size_t served = 0; + keep_alive_max_count == 0 || served < keep_alive_max_count; served++) { auto socket_readable = false; if (!has_buffered_request()) { if (!keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { break; } socket_readable = true; } - auto close_connection = count == 1; + auto close_connection = served + 1 == keep_alive_max_count; auto connection_closed = false; ret = callback(socket_readable, close_connection, connection_closed); if (!ret || connection_closed) { break; } - count--; } return ret; } @@ -10119,7 +10128,10 @@ inline bool range_error(Request &req, Response &res) { // https://www.rfc-editor.org/rfc/rfc9110#section-14.2 // Too many ranges - if (req.ranges.size() > CPPHTTPLIB_RANGE_MAX_COUNT) { return true; } + if (CPPHTTPLIB_RANGE_MAX_COUNT > 0 && + req.ranges.size() > CPPHTTPLIB_RANGE_MAX_COUNT) { + return true; + } for (auto &r : req.ranges) { auto &first_pos = r.first; @@ -13844,8 +13856,10 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection, } else { std::string s = "timeout="; s += std::to_string(keep_alive_timeout_sec_); - s += ", max="; - s += std::to_string(keep_alive_max_count_); + if (keep_alive_max_count_ > 0) { + s += ", max="; + s += std::to_string(keep_alive_max_count_); + } res.set_header("Keep-Alive", s); } @@ -13996,7 +14010,8 @@ inline bool Server::read_content(Stream &strm, Request &req, Response &res) { }, // Multipart FormData [&](const FormData &file) { - if (count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { + if (CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT > 0 && + count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { output_error_log(Error::TooManyFormDataFiles, &req); return false; } @@ -14026,7 +14041,8 @@ inline bool Server::read_content(Stream &strm, Request &req, Response &res) { const auto &content_type = req.get_header_value("Content-Type"); if (detail::case_ignore::equal(detail::extract_media_type(content_type), "application/x-www-form-urlencoded")) { - if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) { + if (CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH > 0 && + req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) { res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414? output_error_log(Error::ExceedMaxPayloadSize, &req); return false; @@ -23219,7 +23235,7 @@ inline WebSocket::~WebSocket() { } inline void WebSocket::start_heartbeat() { - if (ping_interval_sec_ == 0) { return; } + if (ping_interval_sec_ <= 0) { return; } ping_thread_ = std::thread([this]() { std::unique_lock lock(ping_mutex_); // The predicate keeps a spurious wakeup from sending a ping early diff --git a/test/test.cc b/test/test.cc index b5030df1..837163a2 100644 --- a/test/test.cc +++ b/test/test.cc @@ -12457,6 +12457,35 @@ TEST(KeepAliveTest, Issue1041) { EXPECT_EQ(StatusCode::OK_200, result->status); } +TEST(KeepAliveTest, MaxCountZeroSetsNoLimit) { + Server svr; + svr.set_keep_alive_max_count(0); + std::set remote_ports; + svr.Get("/hi", [&](const Request &req, Response &res) { + remote_ports.insert(req.remote_port); + res.set_content("hi", "text/plain"); + }); + + auto listen_thread = std::thread([&svr] { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + listen_thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli(HOST, PORT); + cli.set_keep_alive(true); + for (int i = 0; i < 3; i++) { + auto res = cli.Get("/hi"); + ASSERT_TRUE(res); + EXPECT_EQ("timeout=5", res->get_header_value("Keep-Alive")); + } + // All three over one connection + EXPECT_EQ(1u, remote_ports.size()); +} + TEST(KeepAliveTest, Issue1959) { Server svr; svr.set_keep_alive_timeout(5); @@ -27411,6 +27440,32 @@ TEST(WebSocketTest, AssemblesFragmentsAroundAPing) { EXPECT_EQ(std::string("\x8a\x01p", 3), std::string(pong, 3)); } +TEST(WebSocketTest, NegativePingIntervalSendsNoPings) { + Server svr; + svr.set_websocket_ping_interval(-1); + svr.WebSocket("/ws", [&](const Request &, ws::WebSocket &ws) { + std::string msg; + while (ws.read(msg)) {} + }); + + auto port = svr.bind_to_any_port("127.0.0.1"); + std::thread t([&]() { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + }); + svr.wait_until_ready(); + + auto sock = connect_raw_websocket(port, "/ws"); + ASSERT_NE(INVALID_SOCKET, sock); + auto se_sock = detail::scope_exit([&] { detail::close_socket(sock); }); + + // Nothing comes while the client stays quiet. + detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO, 0, 300000); + char c; + EXPECT_EQ(-1, ::recv(sock, &c, 1, 0)); +} + TEST(WebSocketTest, AnswersAMalformedCloseWithProtocolError) { Server svr; svr.set_websocket_ping_interval(0);