From 3330d0eb06b48a8039d9d78b17514b08eca4127f Mon Sep 17 00:00:00 2001 From: KBS Date: Tue, 29 Sep 2026 04:31:20 +0900 Subject: [PATCH] Ignore an SSE retry field that is not all digits (#2591) parse_sse_line checked only the error code of from_chars, which accepts a leading '-' and stops at the first non-digit, so retry: -1 made the client reconnect without waiting and retry: 10s set 10 ms. The SSE spec ignores a retry value that is not all ASCII digits. --- httplib.h | 3 ++- test/test.cc | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/httplib.h b/httplib.h index d780f352..e64dae0c 100644 --- a/httplib.h +++ b/httplib.h @@ -4914,7 +4914,8 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg, msg.id = value; } else if (field == "retry") { // Parse retry interval in milliseconds - { + // Per the SSE spec, a value that is not all ASCII digits is ignored. + if (detail::is_numeric(value)) { int v = 0; auto res = detail::from_chars(value.data(), value.data() + value.size(), v); diff --git a/test/test.cc b/test/test.cc index a678ebab..62dddece 100644 --- a/test/test.cc +++ b/test/test.cc @@ -22647,6 +22647,64 @@ TEST_F(SSEIntegrationTest, AutoReconnectAfterDisconnect) { EXPECT_GE(message_count.load(), 2); } +// Test: A retry field that is not all ASCII digits is ignored +TEST_F(SSEIntegrationTest, NonDigitRetryFieldIgnored) { + std::atomic connection_count{0}; + + server_->Get("/bad-retry", + [&connection_count](const Request &, Response &res) { + connection_count.fetch_add(1); + res.set_chunked_content_provider( + "text/event-stream", [](size_t offset, DataSink &sink) { + if (offset == 0) { + std::string event = "retry: -1\ndata: hello\n\n"; + sink.write(event.data(), event.size()); + } + return false; + }); + }); + + Client client("localhost", get_port()); + sse::SSEClient sse(client, "/bad-retry"); + + sse.set_reconnect_interval(10000); + sse.start_async(); + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + sse.stop(); + + EXPECT_EQ(connection_count.load(), 1); +} + +// Test: A retry field of all ASCII digits sets the reconnection time +TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) { + std::atomic connection_count{0}; + + server_->Get("/zero-retry", + [&connection_count](const Request &, Response &res) { + connection_count.fetch_add(1); + res.set_chunked_content_provider( + "text/event-stream", [](size_t offset, DataSink &sink) { + if (offset == 0) { + std::string event = "retry: 0\ndata: hello\n\n"; + sink.write(event.data(), event.size()); + } + return false; + }); + }); + + Client client("localhost", get_port()); + sse::SSEClient sse(client, "/zero-retry"); + + sse.set_reconnect_interval(10000); + sse.start_async(); + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + sse.stop(); + + EXPECT_GE(connection_count.load(), 2); +} + // Test: Last-Event-ID sent on reconnect TEST_F(SSEIntegrationTest, LastEventIdSentOnReconnect) { std::atomic connection_count{0};