mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 05:02:29 +07:00
Report only a caller-set WebSocket read timeout as Timeout
199d7eemade read() return the new ReadResult::Timeout for every read timeout and leave the connection open. The compile-time server default (CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 300s) is always in effect, so a handler written as `while (ws.read(msg))`, the form the README's Quick Start uses, no longer ended when a peer went quiet: Timeout is non-zero, so the loop ran its body again with the previous message still in `msg`, and the worker the backstop is meant to reclaim was never released. Nothing caught it because every test of the new result set a timeout explicitly and checked the result by value, and the heartbeat tests keep the connection alive with pings. The two timeouts mean different things. One the caller sets through set_read_timeout() is a request for control back, and is reported as Timeout on a still-open connection. The compile-time default is a backstop against a peer that has gone quiet, and elapsing it is now a failure again: read() returns Fail and closes the connection, as it did before199d7ee. WebSocket tracks whether set_read_timeout() was called, and WebSocketClient carries the same flag over to the WebSocket it creates on connect(). Tests use the heartbeat binary, which compiles both defaults down to 3s: a `while (ws.read(msg))` server handler runs its body once and exits when the client falls silent, and a client that never set a timeout gets Fail with the connection closed. The README and cookbook now say which timeout produces Timeout. Claude-Session: https://claude.ai/code/session_01EF5uZ1X2kaHhqJ8VgfjVaQ
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include <future>
|
||||
|
||||
using namespace httplib;
|
||||
|
||||
class WebSocketHeartbeatTest : public ::testing::Test {
|
||||
@@ -192,6 +194,76 @@ TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
|
||||
EXPECT_FALSE(client.is_open());
|
||||
}
|
||||
|
||||
// The compile-time client read timeout (3s here) was never asked for through
|
||||
// set_read_timeout(), so when it elapses read() reports Fail and closes the
|
||||
// connection rather than handing back a Timeout on a still-open one.
|
||||
TEST_F(WebSocketPongTimeoutTest, CompileTimeClientReadTimeoutIsFail) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
client.set_websocket_ping_interval(0);
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
// Server pings are off and its handler never sends, so nothing arrives.
|
||||
std::string msg;
|
||||
EXPECT_EQ(client.read(msg), ws::Fail);
|
||||
EXPECT_FALSE(client.is_open());
|
||||
}
|
||||
|
||||
// The compile-time server read timeout (3s here) is a backstop that reclaims
|
||||
// the worker from a peer gone quiet, not a timeout the handler asked for. When
|
||||
// it elapses read() must return Fail, so a handler written as
|
||||
// `while (ws.read(msg))` ends instead of re-running its body with the previous
|
||||
// message still in `msg`.
|
||||
class WebSocketServerReadTimeoutTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
svr_.set_websocket_ping_interval(0);
|
||||
svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &ws) {
|
||||
std::string msg;
|
||||
while (ws.read(msg)) {
|
||||
iterations_++;
|
||||
ws.send(msg);
|
||||
}
|
||||
handler_done_.set_value();
|
||||
});
|
||||
|
||||
port_ = svr_.bind_to_any_port("localhost");
|
||||
thread_ = std::thread([this]() { svr_.listen_after_bind(); });
|
||||
svr_.wait_until_ready();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
svr_.stop();
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
Server svr_;
|
||||
int port_;
|
||||
std::thread thread_;
|
||||
std::atomic<int> iterations_{0};
|
||||
std::promise<void> handler_done_;
|
||||
};
|
||||
|
||||
TEST_F(WebSocketServerReadTimeoutTest, BackstopEndsHandlerLoop) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
client.set_websocket_ping_interval(0); // nothing reaches the server's read()
|
||||
client.set_read_timeout(10, 0); // fail rather than hang
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
ASSERT_TRUE(client.send("hello"));
|
||||
std::string msg;
|
||||
ASSERT_EQ(client.read(msg), ws::Text);
|
||||
EXPECT_EQ("hello", msg);
|
||||
|
||||
// The client now stays silent. The server's backstop elapses and the
|
||||
// handler returns, having run its loop body exactly once.
|
||||
auto done = handler_done_.get_future();
|
||||
ASSERT_EQ(done.wait_for(std::chrono::seconds(6)), std::future_status::ready);
|
||||
EXPECT_EQ(1, iterations_.load());
|
||||
|
||||
EXPECT_EQ(client.read(msg), ws::Fail);
|
||||
EXPECT_FALSE(client.is_open());
|
||||
}
|
||||
|
||||
// Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
|
||||
// even with a small max_missed_pongs budget. This is the positive counterpart
|
||||
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so
|
||||
|
||||
Reference in New Issue
Block a user