From cadcc425b8531669e699d368aae5b4fbda412670 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 16:39:00 -0400 Subject: [PATCH] Bound the size of an SSE event The SSE client buffered a line, or the data of an event, with no limit of its own, and with payload_max_length(0) with none at all. Add CPPHTTPLIB_SSE_MAX_EVENT_LENGTH (16MB) and end the connection with ExceedMaxPayloadSize past it. --- README-sse.md | 2 ++ httplib.h | 13 +++++++++++++ test/test.cc | 29 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/README-sse.md b/README-sse.md index 47607ea7..3c66caa0 100644 --- a/README-sse.md +++ b/README-sse.md @@ -79,6 +79,8 @@ sse.set_max_reconnect_attempts(10); sse.set_headers({{"Authorization", "Bearer new_token"}}); ``` +An event, or a line still waiting for its end, longer than `CPPHTTPLIB_SSE_MAX_EVENT_LENGTH` (16MB by default, `0` for no limit) ends the connection with `Error::ExceedMaxPayloadSize`, and the client reconnects as usual. + #### Control ```cpp diff --git a/httplib.h b/httplib.h index 59db65bb..bcd34cf1 100644 --- a/httplib.h +++ b/httplib.h @@ -138,6 +138,10 @@ #define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (100 * 1024 * 1024) // 100MB #endif +#ifndef CPPHTTPLIB_SSE_MAX_EVENT_LENGTH +#define CPPHTTPLIB_SSE_MAX_EVENT_LENGTH (16 * 1024 * 1024) // 16MB +#endif + #ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH #define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192 #endif @@ -5084,6 +5088,15 @@ inline void SSEClient::run_event_loop() { // Keep unprocessed data in buffer buffer.erase(0, line_start); + + // Neither an event nor a line still waiting for its end may grow + // without bound. 0 means no limit. + if (CPPHTTPLIB_SSE_MAX_EVENT_LENGTH > 0 && + buffer.size() + current_msg.data.size() > + CPPHTTPLIB_SSE_MAX_EVENT_LENGTH) { + if (on_error_) { on_error_(Error::ExceedMaxPayloadSize); } + break; + } } // Connection ended diff --git a/test/test.cc b/test/test.cc index 22d8a18b..ac7018f7 100644 --- a/test/test.cc +++ b/test/test.cc @@ -23809,6 +23809,35 @@ TEST_F(SSEIntegrationTest, SuccessfulConnection) { EXPECT_TRUE(message_received.load()); } +TEST_F(SSEIntegrationTest, TooLongEventEndsTheConnection) { + // A data line that never ends. + server_->Get("/endless-line", [](const Request &, Response &res) { + res.set_chunked_content_provider( + "text/event-stream", [](size_t offset, DataSink &sink) { + if (offset == 0) { sink.write("data: ", 6); } + std::string chunk(64 * 1024, 'A'); + return sink.write(chunk.data(), chunk.size()); + }); + }); + + Client client("localhost", get_port()); + // A stream that may run forever, so that only the event is bounded. + client.set_payload_max_length(0); + sse::SSEClient sse(client, "/endless-line"); + std::atomic too_long{false}; + sse.on_error([&](Error err) { + if (err == Error::ExceedMaxPayloadSize) { too_long = true; } + }); + sse.set_max_reconnect_attempts(1); + sse.start_async(); + + for (int i = 0; i < 500 && !too_long; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + sse.stop(); + EXPECT_TRUE(too_long); +} + // Test: on_message callback TEST_F(SSEIntegrationTest, OnMessageCallback) { // Endpoint that sends multiple events then closes