Browse Source

Ignore an SSE id that cannot be sent as Last-Event-ID

SSEClient stored any id it received, but the client rejects a request
whose header value is invalid. An id with a NUL, a control character or
surrounding whitespace therefore made every reconnect fail with
Error::InvalidHeaders before reaching the server, and the id could
never be replaced.

Accept an id only if it is a valid header value, so that the previous
id stays in effect. The SSE spec asks for this for NUL only; the rest
follows from what this client can send.
yhirose 2 days ago
parent
commit
5189edc52e
2 changed files with 28 additions and 3 deletions
  1. 6 3
      httplib.h
  2. 22 0
      test/test.cc

+ 6 - 3
httplib.h

@@ -4918,9 +4918,12 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
     msg.data += value;
     msg.data += value;
     has_data = true;
     has_data = true;
   } else if (field == "id") {
   } else if (field == "id") {
-    // Empty id is valid (clears the last event ID)
-    msg.id = value;
-    has_id = true;
+    // An id that cannot be sent back as Last-Event-ID is ignored. Empty id is
+    // valid (clears the last event ID)
+    if (detail::fields::is_field_value(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.

+ 22 - 0
test/test.cc

@@ -22869,6 +22869,28 @@ TEST_F(SSEParsingTest, EmptyEventIdClearsLastEventId) {
   EXPECT_EQ(msgs[2].data, "not sent");
   EXPECT_EQ(msgs[2].data, "not sent");
 }
 }
 
 
+TEST_F(SSEParsingTest, UnsendableEventIdKeepsLastEventId) {
+  // The first connection sets an id and then sends ids that cannot be carried
+  // in a header; the second echoes back the Last-Event-ID it was reconnected
+  // with
+  std::atomic<int> connection_count{0};
+  server_->Get("/id-unsendable", [&](const Request &req, Response &res) {
+    if (connection_count++ == 0) {
+      res.set_content(std::string("id: 42\n\nid: a") + '\0' +
+                          "b\n\nid: \x01\n\nid:  7\n\nid: 7 \n\n",
+                      "text/event-stream");
+    } else {
+      res.set_content("data: " + req.get_header_value("Last-Event-ID") +
+                          "\n\nevent: end\ndata: end\n\n",
+                      "text/event-stream");
+    }
+  });
+
+  auto msgs = collect("/id-unsendable");
+  ASSERT_EQ(msgs.size(), 1u);
+  EXPECT_EQ(msgs[0].data, "42");
+}
+
 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");