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.
This commit is contained in:
yhirose committed 2026-10-16 21:38:40 -04:00
1 parent ad6c851d19
commit f29721c7d8
5 files changed
+92 -20

No files matched your search

+8 -7
View File
@@ -764,11 +764,12 @@ svr.Post("/content_receiver",
}); });
``` ```
`CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT` (default 1024) caps the number of `CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT` (default 1024, `0` for no limit)
form-data parts only on the buffered path, where every part is accumulated into caps the number of form-data parts only on the buffered path, where every part
`req.form`. The content receiver keeps nothing, so the cap does not apply here. is accumulated into `req.form`. The content receiver keeps nothing, so the cap
If your handler needs an upper bound on the number of parts, count them yourself does not apply here. If your handler needs an upper bound on the number of
and return `false` from the callback to stop the parser. parts, count them yourself and return `false` from the callback to stop the
parser.
### Send content with the content provider ### Send content with the content provider
@@ -884,7 +885,7 @@ svr.set_expect_100_continue_handler([](const Request &req, Response &res) {
### Keep-Alive connection ### Keep-Alive connection
```cpp ```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 svr.set_keep_alive_timeout(10); // Default is 5
``` ```
@@ -909,7 +910,7 @@ svr.set_payload_max_length(1024 * 1024 * 512); // 512MB
``` ```
> [!NOTE] > [!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 ### Server-Sent Events
+1 -1
View File
@@ -10,7 +10,7 @@ status: "draft"
| API | Default | Meaning | | 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 | | `set_keep_alive_timeout` | 5s | How long an idle connection is kept before closing |
## Basic usage ## Basic usage
+1 -1
View File
@@ -10,7 +10,7 @@ status: "draft"
| API | デフォルト | 意味 | | API | デフォルト | 意味 |
| --- | --- | --- | | --- | --- | --- |
| `set_keep_alive_max_count` | 100 | 1本の接続で受け付けるリクエストの最大数 | | `set_keep_alive_max_count` | 100 | 1本の接続で受け付けるリクエストの最大数(0 で無制限) |
| `set_keep_alive_timeout` | 5秒 | アイドル状態の接続を閉じるまでの秒数 | | `set_keep_alive_timeout` | 5秒 | アイドル状態の接続を閉じるまでの秒数 |
## 基本の使い方 ## 基本の使い方
+27 -11
View File
@@ -223,6 +223,16 @@
#define CPPHTTPLIB_MAX_LINE_LENGTH 32768 #define CPPHTTPLIB_MAX_LINE_LENGTH 32768
#endif #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 #ifndef CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH
#define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216 #define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216
#endif #endif
@@ -6826,20 +6836,19 @@ inline bool process_server_socket_core(const std::atomic<socket_t> &svr_sock,
size_t keep_alive_max_count, size_t keep_alive_max_count,
time_t keep_alive_timeout_sec, time_t keep_alive_timeout_sec,
P has_buffered_request, T callback) { P has_buffered_request, T callback) {
assert(keep_alive_max_count > 0);
auto ret = false; auto ret = false;
auto count = keep_alive_max_count; // A keep_alive_max_count of 0 sets no limit.
while (count > 0) { for (size_t served = 0;
keep_alive_max_count == 0 || served < keep_alive_max_count; served++) {
auto socket_readable = false; auto socket_readable = false;
if (!has_buffered_request()) { if (!has_buffered_request()) {
if (!keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { break; } if (!keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { break; }
socket_readable = true; socket_readable = true;
} }
auto close_connection = count == 1; auto close_connection = served + 1 == keep_alive_max_count;
auto connection_closed = false; auto connection_closed = false;
ret = callback(socket_readable, close_connection, connection_closed); ret = callback(socket_readable, close_connection, connection_closed);
if (!ret || connection_closed) { break; } if (!ret || connection_closed) { break; }
count--;
} }
return ret; return ret;
} }
@@ -10119,7 +10128,10 @@ inline bool range_error(Request &req, Response &res) {
// https://www.rfc-editor.org/rfc/rfc9110#section-14.2 // https://www.rfc-editor.org/rfc/rfc9110#section-14.2
// Too many ranges // 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) { for (auto &r : req.ranges) {
auto &first_pos = r.first; auto &first_pos = r.first;
@@ -13844,8 +13856,10 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
} else { } else {
std::string s = "timeout="; std::string s = "timeout=";
s += std::to_string(keep_alive_timeout_sec_); s += std::to_string(keep_alive_timeout_sec_);
s += ", max="; if (keep_alive_max_count_ > 0) {
s += std::to_string(keep_alive_max_count_); s += ", max=";
s += std::to_string(keep_alive_max_count_);
}
res.set_header("Keep-Alive", s); res.set_header("Keep-Alive", s);
} }
@@ -13996,7 +14010,8 @@ inline bool Server::read_content(Stream &strm, Request &req, Response &res) {
}, },
// Multipart FormData // Multipart FormData
[&](const FormData &file) { [&](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); output_error_log(Error::TooManyFormDataFiles, &req);
return false; 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"); const auto &content_type = req.get_header_value("Content-Type");
if (detail::case_ignore::equal(detail::extract_media_type(content_type), if (detail::case_ignore::equal(detail::extract_media_type(content_type),
"application/x-www-form-urlencoded")) { "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? res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414?
output_error_log(Error::ExceedMaxPayloadSize, &req); output_error_log(Error::ExceedMaxPayloadSize, &req);
return false; return false;
@@ -23219,7 +23235,7 @@ inline WebSocket::~WebSocket() {
} }
inline void WebSocket::start_heartbeat() { inline void WebSocket::start_heartbeat() {
if (ping_interval_sec_ == 0) { return; } if (ping_interval_sec_ <= 0) { return; }
ping_thread_ = std::thread([this]() { ping_thread_ = std::thread([this]() {
std::unique_lock<std::mutex> lock(ping_mutex_); std::unique_lock<std::mutex> lock(ping_mutex_);
// The predicate keeps a spurious wakeup from sending a ping early // The predicate keeps a spurious wakeup from sending a ping early
+55
View File
@@ -12457,6 +12457,35 @@ TEST(KeepAliveTest, Issue1041) {
EXPECT_EQ(StatusCode::OK_200, result->status); EXPECT_EQ(StatusCode::OK_200, result->status);
} }
TEST(KeepAliveTest, MaxCountZeroSetsNoLimit) {
Server svr;
svr.set_keep_alive_max_count(0);
std::set<int> 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) { TEST(KeepAliveTest, Issue1959) {
Server svr; Server svr;
svr.set_keep_alive_timeout(5); svr.set_keep_alive_timeout(5);
@@ -27411,6 +27440,32 @@ TEST(WebSocketTest, AssemblesFragmentsAroundAPing) {
EXPECT_EQ(std::string("\x8a\x01p", 3), std::string(pong, 3)); 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) { TEST(WebSocketTest, AnswersAMalformedCloseWithProtocolError) {
Server svr; Server svr;
svr.set_websocket_ping_interval(0); svr.set_websocket_ping_interval(0);