diff --git a/httplib.h b/httplib.h index 31fc1143..8ff4da18 100644 --- a/httplib.h +++ b/httplib.h @@ -4376,7 +4376,8 @@ public: void stop(); private: - bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms); + bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms, + bool &has_data); void run_event_loop(); void dispatch_event(const SSEMessage &msg); bool should_reconnect(int count) const; @@ -4886,7 +4887,7 @@ inline void SSEClient::stop() { } inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg, - int &retry_ms) { + int &retry_ms, bool &has_data) { // Blank line signals end of event if (line.empty() || line == "\r") { return true; } @@ -4895,16 +4896,11 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg, // Find the colon separator auto colon_pos = line.find(':'); - if (colon_pos == std::string::npos) { - // Line with no colon is treated as field name with empty value - return false; - } - auto field = line.substr(0, colon_pos); std::string value; // Value starts after colon, skip optional single space - if (colon_pos + 1 < line.size()) { + if (colon_pos != std::string::npos && colon_pos + 1 < line.size()) { auto value_start = colon_pos + 1; if (line[value_start] == ' ') { value_start++; } value = line.substr(value_start); @@ -4917,8 +4913,9 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg, msg.event = value; } else if (field == "data") { // Multiple data lines are concatenated with newlines - if (!msg.data.empty()) { msg.data += "\n"; } + if (has_data) { msg.data += "\n"; } msg.data += value; + has_data = true; } else if (field == "id") { // Empty id is valid (clears the last event ID) msg.id = value; @@ -4992,6 +4989,7 @@ inline void SSEClient::run_event_loop() { // Event receiving loop std::string buffer; SSEMessage current_msg; + bool has_data = false; while (running_.load() && result.next()) { buffer.append(result.data(), result.size()); @@ -5007,9 +5005,9 @@ inline void SSEClient::run_event_loop() { // Parse the line and check if event is complete auto event_complete = - parse_sse_line(line, current_msg, reconnect_interval_ms_); + parse_sse_line(line, current_msg, reconnect_interval_ms_, has_data); - if (event_complete && !current_msg.data.empty()) { + if (event_complete && has_data) { // Update last_event_id for reconnection if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; } @@ -5017,6 +5015,7 @@ inline void SSEClient::run_event_loop() { dispatch_event(current_msg); current_msg.clear(); + has_data = false; } } diff --git a/test/test.cc b/test/test.cc index 21952987..79357a47 100644 --- a/test/test.cc +++ b/test/test.cc @@ -22678,6 +22678,45 @@ TEST_F(SSEIntegrationTest, MultiLineDataIntegration) { EXPECT_EQ(received_data, "line1\nline2\nline3"); } +TEST_F(SSEIntegrationTest, EmptyDataLines) { + server_->Get("/empty-data-lines", [](const Request &, Response &res) { + res.set_chunked_content_provider("text/event-stream", [](size_t offset, + DataSink &sink) { + if (offset == 0) { + const std::string events = "data:\n\ndata:\ndata: hello\n\ndata\n\n"; + sink.write(events.data(), events.size()); + } + return false; + }); + }); + + Client client("localhost", get_port()); + sse::SSEClient sse(client, "/empty-data-lines"); + std::mutex mutex; + std::condition_variable cv; + std::vector received; + + sse.on_message([&](const sse::SSEMessage &msg) { + std::lock_guard lock(mutex); + received.push_back(msg.data); + cv.notify_all(); + }); + sse.set_max_reconnect_attempts(1); + sse.start_async(); + + { + std::unique_lock lock(mutex); + cv.wait_for(lock, std::chrono::seconds(2), + [&] { return received.size() >= 3; }); + } + sse.stop(); + + ASSERT_GE(received.size(), 3u); + EXPECT_EQ(received[0], ""); + EXPECT_EQ(received[1], "\nhello"); + EXPECT_EQ(received[2], ""); +} + // Test: Auto-reconnect after server disconnection TEST_F(SSEIntegrationTest, AutoReconnectAfterDisconnect) { std::atomic connection_count{0};