Allow http workers to send data optimistically as an optimization
What changed, and why it matters
This commit changes how Bitcoin Core's built-in HTTP server sends replies. Previously, worker threads always queued response data and told the main I/O loop to send it later. Now, if the send buffer is empty, the worker tries to push data straight to the client socket immediately ('optimistic send'). To allow this safely across threads, one internal flag was changed to an atomic variable. The change is described by the author as a performance optimization, not a security fix.
Treat as a routine optimization commit. Reviewers should verify that MaybeSendBytesFromBuffer() and the surrounding send-buffer/connection-state logic remain correct under concurrent worker-thread and I/O-loop access, including edge cases around partial sends, disconnects, and keep-alive connections. No immediate security action is indicated by the supplied materials.
Security signals we found
Cross-thread state access: a worker thread now calls socket-send logic and sets flags previously managed by the I/O loop
Atomic conversion of m_connection_busy indicates awareness of concurrent access
No explicit security claims, CVE references, or bug-report attribution in commit or supplied references
Change touches network I/O and connection-lifecycle state (m_send_ready, m_connection_busy, m_send_buffer)
Evidence from the diff
The patch modifies HTTPRequest::WriteReply() in src/httpserver.cpp. It records whether m_send_buffer was empty before appending the reply. If it was empty, it calls HTTPRemoteClient::MaybeSendBytesFromBuffer() directly from the worker thread; otherwise it sets m_send_ready for the I/O loop. MaybeSendBytesFromBuffer() is updated so that when a partial send occurs it sets m_send_ready and m_connection_busy. Because m_connection_busy can now be set by worker threads as well as the I/O loop, it is changed from bool to std::atomic_bool in src/httpserver.h. The commit message frames this purely as an optimization.
Changed components
src/httpserver.cppsrc/httpserver.hHTTPRequest::WriteReplyHTTPRemoteClient::MaybeSendBytesFromBufferHTTPRemoteClient::m_connection_busyInspect captured patch +21 / −6
diff --git a/src/httpserver.cpp b/src/httpserver.cpp
index 8467c1f9..a5d56be0 100644
--- a/src/httpserver.cpp
+++ b/src/httpserver.cpp
@@ -1040,9 +1040,11 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span<const std::byte> r
const std::string headers{res.StringifyHeaders()};
const auto headers_bytes{std::as_bytes(std::span{headers})};
+ bool send_buffer_was_empty{false};
// Fill the send buffer with the complete serialized response headers + body
{
LOCK(m_client->m_send_mutex);
+ send_buffer_was_empty = m_client->m_send_buffer.empty();
m_client->m_send_buffer.insert(m_client->m_send_buffer.end(), headers_bytes.begin(), headers_bytes.end());
// We've been using std::span up until now but it is finally time to copy
@@ -1051,10 +1053,6 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span<const std::byte> r
m_client->m_send_buffer.insert(m_client->m_send_buffer.end(), reply_body.begin(), reply_body.end());
}
- // Inform HTTPServer I/O loop that there is data that is ready to be sent to
- // this client in the next loop iteration.
- m_client->m_send_ready = true;
-
LogDebug(
BCLog::HTTP,
"HTTPResponse (status code: %d size: %lld) added to send buffer for client %s (id=%llu)",
@@ -1062,6 +1060,18 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span<const std::byte> r
headers_bytes.size() + reply_body.size(),
m_client->m_origin,
m_client->m_id);
+
+ // If the send buffer was empty before we wrote this reply, we can try an
+ // optimistic send akin to CConnman::PushMessage() in which we
+ // push the data directly out the socket to client right now, instead
+ // of waiting for the next iteration of the I/O loop.
+ if (send_buffer_was_empty) {
+ m_client->MaybeSendBytesFromBuffer();
+ } else {
+ // Inform HTTPServer I/O that data is ready to be sent to this client
+ // in the next loop iteration.
+ m_client->m_send_ready = true;
+ }
}
util::Expected<void, std::string> HTTPServer::BindAndStartListening(const CService& to)
@@ -1536,6 +1546,10 @@ bool HTTPRemoteClient::MaybeSendBytesFromBuffer()
// Do not attempt to read from this client.
return false;
}
+ } else {
+ // The send buffer isn't flushed yet, try to push more on the next loop.
+ m_send_ready = true;
+ m_connection_busy = true;
}
}
diff --git a/src/httpserver.h b/src/httpserver.h
index 4018837d..cbb9dd43 100644
--- a/src/httpserver.h
+++ b/src/httpserver.h
@@ -573,9 +573,10 @@ public:
//! Initialized to true while server waits for first request from client.
//! Set to false after data is written to m_send_buffer and then that buffer is flushed to client.
//! Reset to true when we receive new request data from client.
- //! Checked during DisconnectClients(). All of these operations take place in the HTTPServer I/O loop.
+ //! Checked during DisconnectClients() and set by read/write operations
+ //! called in either the HTTPServer I/O loop or by a worker thread during an "optimistic send".
//! `m_connection_busy=true` can be overridden by `m_disconnect=true` (we disconnect).
- bool m_connection_busy{true};
+ std::atomic_bool m_connection_busy{true};
//! Client has requested to keep the connection open after all requests have been responded to.
//! Set by (potentially multiple) worker threads and checked in the HTTPServer I/O loop.
Why this scored 27/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.