Skip to content

fix: Use Websocket ping - pong to detect a stuck connection - #7168

Merged
pajlada merged 7 commits into
Chatterino:masterfrom
invisibl3cat:ws-ping-probes
Sep 12, 2026
Merged

pajlada merged 7 commits into
Chatterino:masterfrom
invisibl3cat:ws-ping-probes

Conversation

@invisibl3cat

Copy link
Copy Markdown
Contributor

If a Websocket connection fails in a way that the connection appears serviceable from the application's perspective but cannot really transfer any data, there is currently no way for us to detect this. This may happen after some network configuration changes or if there is an issue somewhere along the way between us and the remote peer.
We can leverage Websocket ping and pong frames to periodically probe the remote peer if we do not see any other traffic going through the Websocket connection.

Comment thread src/common/websockets/detail/WebSocketConnectionImpl.cpp Outdated
// going through the socket
static constexpr std::chrono::seconds HEALTH_CHECK_INTERVAL_LONG =
std::chrono::seconds{60};
// Use a short intervat when there is no traffic except for the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Use a short intervat when there is no traffic except for the
// Use a short interval when there is no traffic except for the

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions


this->stream.control_callback(
boost::bind(&WebSocketConnectionHelper::onControlFrame, this,
boost::placeholders::_1, boost::placeholders::_2));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: prefer a lambda to boost::bind [modernize-avoid-bind]

Suggested change
boost::placeholders::_1, boost::placeholders::_2));
[this] { onControlFrame(boost::placeholders::_1, boost::placeholders::_2); });

this->healthCheckTimer.expires_after(HEALTH_CHECK_INTERVAL_LONG);
this->healthCheckTimer.async_wait(
boost::bind(&WebSocketConnectionHelper::onHealthCheck,
this->shared_from_this(), boost::placeholders::_1));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: prefer a lambda to boost::bind [modernize-avoid-bind]

Suggested change
this->shared_from_this(), boost::placeholders::_1));
[capture0 = this->shared_from_this()] { capture0->onHealthCheck(boost::placeholders::_1); });

return;
}

this->stream.async_ping({}, [](const boost::system::error_code ec) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: Missing reference on large type (sizeof const boost::system::error_code ec is 24 bytes) [clazy-function-args-by-ref]

Suggested change
this->stream.async_ping({}, [](const boost::system::error_code ec) {
this->stream.async_ping({}, [](const boost::system::error_code &ec) {

this->healthCheckTimer.expires_after(HEALTH_CHECK_INTERVAL_SHORT);
this->healthCheckTimer.async_wait(
boost::bind(&WebSocketConnectionHelper::onHealthCheck,
this->shared_from_this(), boost::placeholders::_1));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: prefer a lambda to boost::bind [modernize-avoid-bind]

Suggested change
this->shared_from_this(), boost::placeholders::_1));
[capture0 = this->shared_from_this()] { capture0->onHealthCheck(boost::placeholders::_1); });

BalancedResolverResults resolvedEndpoints;

boost::asio::steady_timer healthCheckTimer;
int pingProbesTried;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use an unsigned type and initialize it here.

Comment on lines +26 to +29
// Use a short interval when there is no traffic except for the
// ping probe so that we can detect a failed socket quickly
static constexpr std::chrono::seconds HEALTH_CHECK_INTERVAL_SHORT =
std::chrono::seconds{5};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment and the variable name are confusing. They make it sounds like the ping check has two different modes that depend on the application messages. So if the application doesn't send/receive anything, we'd send pings every 5s.

Looking at the code, that's not what we do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, the current text is a bit misleading. Updated phrasing should describe the actual logic correctly.

Comment on lines +264 to +265
boost::bind(&WebSocketConnectionHelper::onControlFrame, this,
boost::placeholders::_1, boost::placeholders::_2));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't this and the one below use beast::bind_front_handler?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just replaced the binds with lambdas.

if (frame_type == boost::beast::websocket::frame_type::pong)
{
this->pingProbesTried = 0;
this->healthCheckTimer.expires_after(HEALTH_CHECK_INTERVAL_LONG);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whenever we call expires_after on a steady timer, it will cancel any existing async_wait calls. This means onHealthCheck will never be called after this.
We will need some mechanism to handle this case - either don't reset the timer, or explicitly call async_wait again

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang, such a lame mistake :) It should be okay now.

FTR it is possible to not see the periodic pinging if there is other data being regularly received over the socket. But that doesn't seem to happen in practice.

@pajlada
pajlada enabled auto-merge (squash) September 12, 2026 10:43
@pajlada
pajlada merged commit 78eee1f into Chatterino:master Sep 12, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants