Wake a WebSocket read on a pong timeout on every platform

When max_missed_pongs ran out, the heartbeat thread shut down the read
side of the socket to end a read() blocked on the silent peer. On
Windows that does not wake the wait, so read() stayed blocked until its
read timeout and then returned Timeout. The heartbeat test never ran on
Windows, so CI did not catch it.

The heartbeat now sets a flag that the stream's read interrupt checks,
the mechanism SSEClient::stop() uses, and WebSocketSSLStream takes the
interrupt too. It is set only when a pong timeout can happen, so other
connections wait as before. read() reports a timeout on a connection
that has closed as Fail, since Timeout means it is still usable.

Build and run the heartbeat test in the Windows jobs, and add a TLS
pong timeout test.
This commit is contained in:
yhirose committed 2026-10-18 17:23:05 -04:00
1 parent 801bf2e0d2
commit 696b94b5ed
4 files changed
+76 -27

No files matched your search

+37 -12
View File
@@ -146,16 +146,21 @@ TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
// ping count reaches max_missed_pongs.
class WebSocketPongTimeoutTest : public ::testing::Test {
protected:
virtual std::unique_ptr<Server> make_server() {
return std::unique_ptr<Server>(new Server());
}
void SetUp() override {
svr_.set_websocket_ping_interval(0);
svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &) {
svr_ = make_server();
svr_->set_websocket_ping_interval(0);
svr_->WebSocket("/ws", [this](const Request &, ws::WebSocket &) {
std::unique_lock<std::mutex> lock(handler_mutex_);
handler_cv_.wait(lock, [this]() { return release_; });
});
port_ = svr_.bind_to_any_port("localhost");
thread_ = std::thread([this]() { svr_.listen_after_bind(); });
svr_.wait_until_ready();
port_ = svr_->bind_to_any_port("localhost");
thread_ = std::thread([this]() { svr_->listen_after_bind(); });
svr_->wait_until_ready();
}
void TearDown() override {
@@ -164,11 +169,11 @@ protected:
release_ = true;
}
handler_cv_.notify_all();
svr_.stop();
svr_->stop();
thread_.join();
}
Server svr_;
std::unique_ptr<Server> svr_;
int port_;
std::thread thread_;
std::mutex handler_mutex_;
@@ -176,8 +181,10 @@ protected:
bool release_ = false;
};
TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
// Client pings every 1s (compile-time default in this test file). With
// max_missed_pongs = 2, the heartbeat thread should self-close within roughly
// 3s, and that has to end a read() already waiting on the peer.
static void expect_pong_timeout_ends_read(ws::WebSocketClient &client) {
client.set_websocket_max_missed_pongs(2);
// A read timeout asked for at runtime is reported as Timeout, so it cannot
// be mistaken for the Fail a pong timeout produces.
@@ -185,9 +192,6 @@ TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.is_open());
// Client pings every 1s (compile-time default in this test file).
// With max_missed_pongs = 2, the heartbeat thread should self-close within
// roughly 3s, and that has to end a read() already waiting on the peer.
auto start = std::chrono::steady_clock::now();
std::string msg;
EXPECT_EQ(client.read(msg), ws::Fail);
@@ -196,6 +200,27 @@ TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
EXPECT_FALSE(client.is_open());
}
TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
expect_pong_timeout_ends_read(client);
}
#ifdef CPPHTTPLIB_SSL_ENABLED
class WebSocketPongTimeoutSSLTest : public WebSocketPongTimeoutTest {
protected:
std::unique_ptr<Server> make_server() override {
return std::unique_ptr<Server>(new SSLServer("./cert.pem", "./key.pem"));
}
};
TEST_F(WebSocketPongTimeoutSSLTest, ClientDetectsNonResponsivePeer) {
ws::WebSocketClient client("wss://localhost:" + std::to_string(port_) +
"/ws");
client.enable_server_certificate_verification(false);
expect_pong_timeout_ends_read(client);
}
#endif
// 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.