mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-18 11:09:39 +00:00
Fix SSE client not clearing Last-Event-ID on an empty id field (Fix #2611)
An event with an empty id field must reset the last event ID, so that no Last-Event-ID header is sent on reconnect. run_event_loop only updated last_event_id_ when the id was non-empty, so it could not tell an empty id field from an event with no id field, and kept sending the stale ID. Track whether an id field was seen with a has_id flag, as has_data does for the data field, and add a regression test.
This commit is contained in:
1 parent
cf3693cb5c
commit
eda9a10bfe
2 files changed
+35
-7
No files matched your search
@@ -4377,7 +4377,7 @@ public:
|
|||||||
|
|
||||||
private:
|
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);
|
bool &has_data, bool &has_id);
|
||||||
void run_event_loop();
|
void run_event_loop();
|
||||||
void dispatch_event(const SSEMessage &msg);
|
void dispatch_event(const SSEMessage &msg);
|
||||||
bool should_reconnect(int count) const;
|
bool should_reconnect(int count) const;
|
||||||
@@ -4887,7 +4887,8 @@ inline void SSEClient::stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||||
int &retry_ms, bool &has_data) {
|
int &retry_ms, bool &has_data,
|
||||||
|
bool &has_id) {
|
||||||
// Blank line signals end of event
|
// Blank line signals end of event
|
||||||
if (line.empty()) { return true; }
|
if (line.empty()) { return true; }
|
||||||
|
|
||||||
@@ -4917,6 +4918,7 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
|||||||
} else if (field == "id") {
|
} else if (field == "id") {
|
||||||
// Empty id is valid (clears the last event ID)
|
// Empty id is valid (clears the last event ID)
|
||||||
msg.id = value;
|
msg.id = value;
|
||||||
|
has_id = true;
|
||||||
} else if (field == "retry") {
|
} else if (field == "retry") {
|
||||||
// Parse retry interval in milliseconds
|
// Parse retry interval in milliseconds
|
||||||
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
||||||
@@ -4987,7 +4989,8 @@ inline void SSEClient::run_event_loop() {
|
|||||||
// Event receiving loop
|
// Event receiving loop
|
||||||
std::string buffer;
|
std::string buffer;
|
||||||
SSEMessage current_msg;
|
SSEMessage current_msg;
|
||||||
bool has_data = false;
|
auto has_data = false;
|
||||||
|
auto has_id = false;
|
||||||
|
|
||||||
while (running_.load() && result.next()) {
|
while (running_.load() && result.next()) {
|
||||||
buffer.append(result.data(), result.size());
|
buffer.append(result.data(), result.size());
|
||||||
@@ -5006,13 +5009,13 @@ inline void SSEClient::run_event_loop() {
|
|||||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||||
|
|
||||||
// Parse the line and check if event is complete
|
// Parse the line and check if event is complete
|
||||||
auto event_complete =
|
auto event_complete = parse_sse_line(
|
||||||
parse_sse_line(line, current_msg, reconnect_interval_ms_, has_data);
|
line, current_msg, reconnect_interval_ms_, has_data, has_id);
|
||||||
|
|
||||||
if (event_complete) {
|
if (event_complete) {
|
||||||
// Update last_event_id for reconnection, even for an event that
|
// Update last_event_id for reconnection, even for an event that
|
||||||
// has no data
|
// has no data. An empty id clears it.
|
||||||
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
if (has_id) { last_event_id_ = current_msg.id; }
|
||||||
|
|
||||||
// An event without a data field is not dispatched
|
// An event without a data field is not dispatched
|
||||||
if (has_data) { dispatch_event(current_msg); }
|
if (has_data) { dispatch_event(current_msg); }
|
||||||
@@ -5020,6 +5023,7 @@ inline void SSEClient::run_event_loop() {
|
|||||||
// Reset the message for the next event either way
|
// Reset the message for the next event either way
|
||||||
current_msg.clear();
|
current_msg.clear();
|
||||||
has_data = false;
|
has_data = false;
|
||||||
|
has_id = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22470,6 +22470,30 @@ TEST_F(SSEParsingTest, EventWithoutDataUpdatesLastEventId) {
|
|||||||
EXPECT_EQ(msgs[0].data, "42");
|
EXPECT_EQ(msgs[0].data, "42");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(SSEParsingTest, EmptyEventIdClearsLastEventId) {
|
||||||
|
// The first connection sets an id and then clears it with an empty one; the
|
||||||
|
// second reports whether it was reconnected with a Last-Event-ID
|
||||||
|
std::atomic<int> connection_count{0};
|
||||||
|
server_->Get("/id-clear", [&](const Request &req, Response &res) {
|
||||||
|
if (connection_count++ == 0) {
|
||||||
|
res.set_content("id: 1\ndata: first\n\nid:\ndata: second\n\n",
|
||||||
|
"text/event-stream");
|
||||||
|
} else {
|
||||||
|
res.set_content(
|
||||||
|
std::string("data: ") +
|
||||||
|
(req.has_header("Last-Event-ID") ? "sent" : "not sent") +
|
||||||
|
"\n\nevent: end\ndata: end\n\n",
|
||||||
|
"text/event-stream");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auto msgs = collect("/id-clear");
|
||||||
|
ASSERT_EQ(msgs.size(), 3u);
|
||||||
|
EXPECT_EQ(msgs[0].id, "1");
|
||||||
|
EXPECT_EQ(msgs[1].id, "");
|
||||||
|
EXPECT_EQ(msgs[2].data, "not sent");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, CompleteEventParsing) {
|
TEST_F(SSEParsingTest, CompleteEventParsing) {
|
||||||
auto msgs = parse("event: notification\nid: evt-42\n"
|
auto msgs = parse("event: notification\nid: evt-42\n"
|
||||||
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
||||||
|
|||||||
Reference in new issue
Block a user