HTTPServer: disconnect after idle timeout (-rpcservertimeout)
What changed, and why it matters
This commit adds an idle timeout for HTTP connections to Bitcoin Core's RPC server. Previously, clients could stay connected indefinitely even when doing nothing. Now, after a configurable period of inactivity (default 30 seconds via -rpcservertimeout), idle clients are disconnected. This is a defensive hardening change that reduces resource exhaustion risks from abandoned or maliciously held-open connections.
No immediate action required. This is a hardening improvement. Operators may review -rpcservertimeout setting if they have long-polling RPC use cases. Reviewers should verify the m_req_busy guard correctly prevents timeout during slow requests and that the atomic timestamp updates are race-safe.
Security signals we found
Adds idle connection timeout to limit resource consumption from idle clients
Guards against disconnecting clients mid-request via m_req_busy check
Uses steady clock for timeout measurement
Resets idle timer on both send and receive activity
Prevents premature shared_ptr destruction while worker thread may still hold reference
Evidence from the diff
The patch implements -rpcservertimeout for the new internal HTTPServer. It tracks per-client last-activity timestamps (m_idle_since) updated on send and receive, and disconnects clients in HTTPServer::DisconnectClients() when inactivity exceeds m_rpcservertimeout, unless a request is still being processed (m_req_busy guard). The timeout is wired to the existing -rpcservertimeout argument with DEFAULT_HTTP_SERVER_TIMEOUT default.
Changed components
src/httpserver.cppsrc/httpserver.hHTTP RPC server connection handlingInspect captured patch +44 / −4
diff --git a/src/httpserver.cpp b/src/httpserver.cpp
index 09dec7ae..3c35eede 100644
--- a/src/httpserver.cpp
+++ b/src/httpserver.cpp
@@ -24,6 +24,7 @@
#include <util/thread.h>
#include <util/threadnames.h>
#include <util/threadpool.h>
+#include <util/time.h>
#include <util/translation.h>
#include <condition_variable>
@@ -1360,6 +1361,9 @@ void HTTPServer::SocketHandlerConnected(const IOReadiness& io_readiness) const
client->m_id);
client->m_disconnect = true;
} else {
+ // Reset idle timeout
+ client->m_idle_since = Now<SteadySeconds>();
+
// Prevent disconnect until all requests are completely handled.
client->m_connection_busy = true;
@@ -1509,12 +1513,27 @@ void HTTPServer::MaybeDispatchRequestsFromClient(const std::shared_ptr<HTTPRemot
void HTTPServer::DisconnectClients()
{
+ const auto now{Now<SteadySeconds>()};
size_t erased = std::erase_if(m_connected,
[&](auto& client) {
- // Disconnect this client due to error or end of communication.
+ // First check for idle timeout. We reset the timer when we send and receive data,
+ // but if the server is busy handling a request we should ignore the timeout until
+ // the reply is sent. If we did erase the shared_ptr<HTTPRemoteClient> reference in m_connected
+ // while the server is busy with a request, there would still be a reference in a worker
+ // thread keeping the socket open even after "disconnecting".
+ const bool is_idle{m_rpcservertimeout.count() > 0 &&
+ now - client->m_idle_since.load() > m_rpcservertimeout &&
+ !client->m_req_busy};
+
+ // Disconnect this client due to error, end of communication, or idle timeout.
// May drop unsent data if we are closing due to error.
- if (client->m_disconnect) {
- ;
+ if (client->m_disconnect || is_idle) {
+ if (is_idle) {
+ LogDebug(BCLog::HTTP,
+ "HTTP client idle timeout %s (id=%llu)",
+ client->m_origin,
+ client->m_id);
+ }
} else {
// Disconnect this client because the server is shutting
// down and we need to disconnect all clients...
@@ -1652,6 +1671,9 @@ bool HTTPRemoteClient::MaybeSendBytesFromBuffer()
m_send_ready = true;
m_connection_busy = true;
}
+
+ // Finally, reset idle timeout
+ m_idle_since = Now<SteadySeconds>();
}
return true;
@@ -1666,6 +1688,8 @@ bool InitHTTPServer()
// Create HTTPServer, using a dummy request handler just for this commit
g_http_server = std::make_unique<HTTPServer>([&](std::unique_ptr<HTTPRequest> req){});
+ g_http_server->SetServerTimeout(std::chrono::seconds(gArgs.GetIntArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT)));
+
// Bind HTTP server to specified addresses
std::vector<std::pair<std::string, uint16_t>> endpoints{GetBindAddresses()};
bool bind_success{false};
diff --git a/src/httpserver.h b/src/httpserver.h
index a498e400..0045178e 100644
--- a/src/httpserver.h
+++ b/src/httpserver.h
@@ -23,6 +23,7 @@
#include <util/strencodings.h>
#include <util/string.h>
#include <util/threadinterrupt.h>
+#include <util/time.h>
namespace util {
class SignalInterrupt;
@@ -413,6 +414,11 @@ public:
*/
void StopAccepting() { m_stop_accepting = true; }
+ /**
+ * Set the idle client timeout (-rpcservertimeout)
+ */
+ void SetServerTimeout(std::chrono::seconds seconds) { m_rpcservertimeout = seconds; }
+
/**
* Force-remove all remaining clients from m_connected without waiting for
* graceful disconnection. Must only be called after JoinSocketsThreads().
@@ -503,6 +509,11 @@ private:
std::function<void(std::unique_ptr<HTTPRequest>&&)> m_request_dispatcher GUARDED_BY(m_request_dispatcher_mutex);
/// @}
+ /**
+ * Idle timeout after which clients are disconnected
+ */
+ std::chrono::seconds m_rpcservertimeout{DEFAULT_HTTP_SERVER_TIMEOUT};
+
/**
* Accept a connection.
* @param[in] listen_sock Socket on which to accept the connection.
@@ -652,8 +663,13 @@ public:
//! possibly overriding all other disconnect flags.
std::atomic_bool m_disconnect{false};
+ //! Timestamp of last send or receive activity, used for -rpcservertimeout.
+ //! Due to optimistic sends it may be updated in either a worker thread or in the
+ //! I/O thread. It is checked in the I/O thread to disconnect idle clients.
+ std::atomic<SteadySeconds> m_idle_since;
+
explicit HTTPRemoteClient(HTTPServer::Id id, const CService& addr, std::unique_ptr<Sock> socket)
- : m_id(id), m_addr(addr), m_origin(addr.ToStringAddrPort()), m_sock{std::move(socket)} {};
+ : m_id(id), m_addr(addr), m_origin(addr.ToStringAddrPort()), m_sock{std::move(socket)}, m_idle_since{Now<SteadySeconds>()} {}
// Disable copies (should only be used as shared pointers)
HTTPRemoteClient(const HTTPRemoteClient&) = delete;
Why this scored 36/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.