Ver Fonte

Treat a bare CR as an SSE line ending

The SSE spec ends a line with CRLF, LF or CR, but SSEClient split only
on LF, so a stream that uses CR alone never produced an event. The LF
of a CRLF is skipped even when it arrives in a later chunk than its CR.

Search for a line ending only in the newly received data. What is left
in the buffer has none, and scanning it again for every chunk made a
long line quadratic.
yhirose há 2 dias atrás
pai
commit
1f12bbaabb
2 ficheiros alterados com 47 adições e 9 exclusões
  1. 16 9
      httplib.h
  2. 31 0
      test/test.cc

+ 16 - 9
httplib.h

@@ -4997,22 +4997,29 @@ inline void SSEClient::run_event_loop() {
     auto has_data = false;
     auto has_id = false;
     auto first_line = true;
+    auto after_cr = false;
 
     while (running_.load() && result.next()) {
+      // What is left in the buffer has no line ending
+      auto search_pos = buffer.size();
       buffer.append(result.data(), result.size());
 
-      // Process complete lines in the buffer
+      // Process complete lines in the buffer. A line ends with \r\n, \n or \r
+      // per the SSE spec
       size_t line_start = 0;
-      size_t newline_pos;
+      size_t eol_pos;
 
-      while ((newline_pos = buffer.find('\n', line_start)) !=
+      while ((eol_pos = buffer.find_first_of("\r\n", search_pos)) !=
              std::string::npos) {
-        auto line = buffer.substr(line_start, newline_pos - line_start);
-        line_start = newline_pos + 1;
-
-        // Strip the \r of a CRLF line ending so that every field, including
-        // one without a colon, sees the same line
-        if (!line.empty() && line.back() == '\r') { line.pop_back(); }
+        auto line = buffer.substr(line_start, eol_pos - line_start);
+        line_start = eol_pos + 1;
+        search_pos = line_start;
+
+        // The \n of a \r\n may arrive in a later chunk than its \r
+        auto is_lf_of_crlf =
+            after_cr && line.empty() && buffer[eol_pos] == '\n';
+        after_cr = buffer[eol_pos] == '\r';
+        if (is_lf_of_crlf) { continue; }
 
         // A UTF-8 BOM at the start of the stream is ignored per the SSE spec
         if (first_line) {

+ 31 - 0
test/test.cc

@@ -22793,6 +22793,37 @@ TEST_F(SSEParsingTest, FieldNameOnlyWithCarriageReturn) {
   EXPECT_EQ(msgs[0].data, "");
 }
 
+TEST_F(SSEParsingTest, CarriageReturnLineEnding) {
+  auto msgs = parse("event: update\rdata: hello\r\r");
+  ASSERT_EQ(msgs.size(), 1u);
+  EXPECT_EQ(msgs[0].event, "update");
+  EXPECT_EQ(msgs[0].data, "hello");
+}
+
+TEST_F(SSEParsingTest, LineEndingSplitAcrossChunks) {
+  // Chunk boundaries fall inside every \r\n and right before the first one
+  std::vector<std::string> chunks = {"event: update", "\r", "\ndata: hello\r",
+                                     "\n\r", "\nevent: end\ndata: end\n\n"};
+  server_->Get("/split", [chunks](const Request &, Response &res) {
+    auto i = std::make_shared<size_t>(0);
+    res.set_chunked_content_provider(
+        "text/event-stream", [chunks, i](size_t /*offset*/, DataSink &sink) {
+          if (*i == chunks.size()) {
+            sink.done();
+          } else {
+            sink.write(chunks[*i].data(), chunks[*i].size());
+            (*i)++;
+          }
+          return true;
+        });
+  });
+
+  auto msgs = collect("/split");
+  ASSERT_EQ(msgs.size(), 1u);
+  EXPECT_EQ(msgs[0].event, "update");
+  EXPECT_EQ(msgs[0].data, "hello");
+}
+
 TEST_F(SSEParsingTest, LeadingByteOrderMarkIgnored) {
   // Only a BOM at the start of the stream is ignored
   auto msgs = parse("\xEF\xBB\xBF"