mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-18 11:09:39 +00:00
1 parent
45820de332
commit
c2bdb1c5c1
3 files changed
+138
-10
No files matched your search
@@ -74,6 +74,9 @@ sse.set_reconnect_interval(5000);
|
|||||||
|
|
||||||
// Set max reconnect attempts (default: 0 = unlimited)
|
// Set max reconnect attempts (default: 0 = unlimited)
|
||||||
sse.set_max_reconnect_attempts(10);
|
sse.set_max_reconnect_attempts(10);
|
||||||
|
|
||||||
|
// Update headers at any time (thread-safe)
|
||||||
|
sse.set_headers({{"Authorization", "Bearer new_token"}});
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Control
|
#### Control
|
||||||
@@ -154,6 +157,25 @@ httplib::sse::SSEClient sse(cli, "/events", headers);
|
|||||||
sse.start();
|
sse.start();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Refreshing Auth Token on Reconnect
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::sse::SSEClient sse(cli, "/events",
|
||||||
|
{{"Authorization", "Bearer " + get_token()}});
|
||||||
|
|
||||||
|
// Preemptively refresh token on each successful connection
|
||||||
|
sse.on_open([&sse]() {
|
||||||
|
sse.set_headers({{"Authorization", "Bearer " + get_token()}});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or reactively refresh on auth failure (401 triggers reconnect)
|
||||||
|
sse.on_error([&sse](httplib::Error) {
|
||||||
|
sse.set_headers({{"Authorization", "Bearer " + refresh_token()}});
|
||||||
|
});
|
||||||
|
|
||||||
|
sse.start();
|
||||||
|
```
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
@@ -3660,6 +3660,9 @@ public:
|
|||||||
SSEClient &set_reconnect_interval(int ms);
|
SSEClient &set_reconnect_interval(int ms);
|
||||||
SSEClient &set_max_reconnect_attempts(int n);
|
SSEClient &set_max_reconnect_attempts(int n);
|
||||||
|
|
||||||
|
// Update headers (thread-safe)
|
||||||
|
SSEClient &set_headers(const Headers &headers);
|
||||||
|
|
||||||
// State accessors
|
// State accessors
|
||||||
bool is_connected() const;
|
bool is_connected() const;
|
||||||
const std::string &last_event_id() const;
|
const std::string &last_event_id() const;
|
||||||
@@ -3684,6 +3687,7 @@ private:
|
|||||||
Client &client_;
|
Client &client_;
|
||||||
std::string path_;
|
std::string path_;
|
||||||
Headers headers_;
|
Headers headers_;
|
||||||
|
mutable std::mutex headers_mutex_;
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
MessageHandler on_message_;
|
MessageHandler on_message_;
|
||||||
@@ -3992,6 +3996,12 @@ inline SSEClient &SSEClient::set_max_reconnect_attempts(int n) {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline SSEClient &SSEClient::set_headers(const Headers &headers) {
|
||||||
|
std::lock_guard<std::mutex> lock(headers_mutex_);
|
||||||
|
headers_ = headers;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool SSEClient::is_connected() const { return connected_.load(); }
|
inline bool SSEClient::is_connected() const { return connected_.load(); }
|
||||||
|
|
||||||
inline const std::string &SSEClient::last_event_id() const {
|
inline const std::string &SSEClient::last_event_id() const {
|
||||||
@@ -4070,7 +4080,11 @@ inline void SSEClient::run_event_loop() {
|
|||||||
|
|
||||||
while (running_.load()) {
|
while (running_.load()) {
|
||||||
// Build headers, including Last-Event-ID if we have one
|
// Build headers, including Last-Event-ID if we have one
|
||||||
auto request_headers = headers_;
|
Headers request_headers;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(headers_mutex_);
|
||||||
|
request_headers = headers_;
|
||||||
|
}
|
||||||
if (!last_event_id_.empty()) {
|
if (!last_event_id_.empty()) {
|
||||||
request_headers.emplace("Last-Event-ID", last_event_id_);
|
request_headers.emplace("Last-Event-ID", last_event_id_);
|
||||||
}
|
}
|
||||||
@@ -4089,19 +4103,19 @@ inline void SSEClient::run_event_loop() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.status() != 200) {
|
if (result.status() != StatusCode::OK_200) {
|
||||||
connected_.store(false);
|
connected_.store(false);
|
||||||
// For certain errors, don't reconnect
|
if (on_error_) { on_error_(Error::Connection); }
|
||||||
if (result.status() == 204 || // No Content - server wants us to stop
|
|
||||||
result.status() == 404 || // Not Found
|
// For certain errors, don't reconnect.
|
||||||
result.status() == 401 || // Unauthorized
|
// Note: 401 is intentionally absent so that handlers can refresh
|
||||||
result.status() == 403) { // Forbidden
|
// credentials via set_headers() and let the client reconnect.
|
||||||
if (on_error_) { on_error_(Error::Connection); }
|
if (result.status() == StatusCode::NoContent_204 ||
|
||||||
|
result.status() == StatusCode::NotFound_404 ||
|
||||||
|
result.status() == StatusCode::Forbidden_403) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (on_error_) { on_error_(Error::Connection); }
|
|
||||||
|
|
||||||
if (!should_reconnect(reconnect_count)) { break; }
|
if (!should_reconnect(reconnect_count)) { break; }
|
||||||
wait_for_reconnect();
|
wait_for_reconnect();
|
||||||
reconnect_count++;
|
reconnect_count++;
|
||||||
|
|||||||
@@ -16007,6 +16007,98 @@ TEST_F(SSEIntegrationTest, LastEventIdSentOnReconnect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test: set_headers updates headers used on reconnect
|
||||||
|
TEST_F(SSEIntegrationTest, SetHeadersUpdatesOnReconnect) {
|
||||||
|
std::vector<std::string> received_tokens;
|
||||||
|
std::mutex token_mutex;
|
||||||
|
|
||||||
|
// Endpoint that captures Authorization header
|
||||||
|
server_->Get("/auth-check", [&](const Request &req, Response &res) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(token_mutex);
|
||||||
|
received_tokens.push_back(req.get_header_value("Authorization"));
|
||||||
|
}
|
||||||
|
res.set_chunked_content_provider(
|
||||||
|
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||||
|
if (offset == 0) {
|
||||||
|
std::string event = "data: hello\n\n";
|
||||||
|
sink.write(event.data(), event.size());
|
||||||
|
}
|
||||||
|
return false; // Close connection to trigger reconnect
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Client client("localhost", get_port());
|
||||||
|
Headers headers = {{"Authorization", "Bearer old-token"}};
|
||||||
|
sse::SSEClient sse(client, "/auth-check", headers);
|
||||||
|
|
||||||
|
// Update headers on each successful connection
|
||||||
|
sse.on_open(
|
||||||
|
[&sse]() { sse.set_headers({{"Authorization", "Bearer new-token"}}); });
|
||||||
|
|
||||||
|
sse.set_reconnect_interval(100);
|
||||||
|
sse.set_max_reconnect_attempts(3);
|
||||||
|
sse.start_async();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||||
|
sse.stop();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(token_mutex);
|
||||||
|
ASSERT_GE(received_tokens.size(), 2u);
|
||||||
|
// First connection uses original header
|
||||||
|
EXPECT_EQ(received_tokens[0], "Bearer old-token");
|
||||||
|
// Second connection uses updated header from set_headers
|
||||||
|
EXPECT_EQ(received_tokens[1], "Bearer new-token");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: 401 allows reconnection (so on_error can refresh headers)
|
||||||
|
TEST_F(SSEIntegrationTest, ReconnectOn401WithHeaderRefresh) {
|
||||||
|
std::atomic<int> connection_count{0};
|
||||||
|
|
||||||
|
// Endpoint: returns 401 on first attempt, 200 on second
|
||||||
|
server_->Get("/auth-retry", [&](const Request &req, Response &res) {
|
||||||
|
int conn = connection_count.fetch_add(1);
|
||||||
|
if (conn == 0 || req.get_header_value("Authorization") != "Bearer valid") {
|
||||||
|
res.status = StatusCode::Unauthorized_401;
|
||||||
|
res.set_content("Unauthorized", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.set_chunked_content_provider(
|
||||||
|
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||||
|
if (offset == 0) {
|
||||||
|
std::string event = "data: authenticated\n\n";
|
||||||
|
sink.write(event.data(), event.size());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Client client("localhost", get_port());
|
||||||
|
Headers headers = {{"Authorization", "Bearer expired"}};
|
||||||
|
sse::SSEClient sse(client, "/auth-retry", headers);
|
||||||
|
|
||||||
|
std::atomic<bool> message_received{false};
|
||||||
|
|
||||||
|
// Refresh token on error
|
||||||
|
sse.on_error(
|
||||||
|
[&sse](Error) { sse.set_headers({{"Authorization", "Bearer valid"}}); });
|
||||||
|
|
||||||
|
sse.on_message([&](const sse::SSEMessage &msg) {
|
||||||
|
if (msg.data == "authenticated") { message_received.store(true); }
|
||||||
|
});
|
||||||
|
|
||||||
|
sse.set_reconnect_interval(100);
|
||||||
|
sse.set_max_reconnect_attempts(3);
|
||||||
|
sse.start_async();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||||
|
sse.stop();
|
||||||
|
|
||||||
|
// Should have reconnected after 401 and succeeded with new token
|
||||||
|
EXPECT_GE(connection_count.load(), 2);
|
||||||
|
EXPECT_TRUE(message_received.load());
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Issue2318Test, EmptyHostString) {
|
TEST(Issue2318Test, EmptyHostString) {
|
||||||
{
|
{
|
||||||
httplib::Client cli_empty("", PORT);
|
httplib::Client cli_empty("", PORT);
|
||||||
|
|||||||
Reference in new issue
Block a user