HTTPServer: use a queue to pipeline requests from each connected client
What changed, and why it matters
This change reorders how Bitcoin Core's built-in web server handles multiple HTTP requests from the same connection. Previously, incoming requests could be handed off to worker threads as soon as they arrived, so a later, fast request might finish and send its response before an earlier, slower one. The patch queues requests per client and processes them strictly one at a time, ensuring responses are sent in the same order requests were received. The commit message frames this as preventing race conditions where a caller could receive stale state.
Treat as a hardening/defensive fix rather than a confirmed vulnerability. Review whether the prior behavior was actually exploitable against RPC endpoints (e.g., wallet state, blockchain tip, or mempool queries returning stale data) and consider whether the change fully closes the race or merely narrows the window. No immediate emergency action is indicated from the diff alone.
Security signals we found
Race condition mitigation: serializes per-client request handling to prevent out-of-order responses
State consistency: commit message explicitly cites risk of 'old state' being returned to later requests
HTTP/1.1 pipelining compliance: references RFC 7230 §6.3.2 response ordering requirement
No input validation, cryptographic, or memory-safety changes visible in diff
Evidence from the diff
The patch modifies HTTPServer to implement per-client HTTP/1.1 pipelining serialization. It adds a std::deque of HTTPRequest pointers and an atomic m_req_busy flag to HTTPRemoteClient. MaybeDispatchRequestsFromClient() now parses all available bytes and enqueues parsed requests, but only dispatches the front of the queue to a worker when no previous request from that client is in flight. WriteReply() clears m_req_busy after the response is fully queued, allowing the next queued request to be dispatched on the next I/O loop iteration. This enforces FIFO request processing and response ordering per connection.
Changed components
src/httpserver.cppsrc/httpserver.hHTTPRemoteClient request queue and busy flagHTTPServer::MaybeDispatchRequestsFromClientHTTPRequest::WriteReplyInspect captured patch +33 / −4
diff --git a/src/httpserver.cpp b/src/httpserver.cpp
index a5d56be0..ad6d020d 100644
--- a/src/httpserver.cpp
+++ b/src/httpserver.cpp
@@ -1072,6 +1072,9 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span<const std::byte> r
// in the next loop iteration.
m_client->m_send_ready = true;
}
+
+ // Signal to the I/O loop that we are ready to handle the next request.
+ m_client->m_req_busy = false;
}
util::Expected<void, std::string> HTTPServer::BindAndStartListening(const CService& to)
@@ -1293,10 +1296,13 @@ void HTTPServer::SocketHandlerConnected(const IOReadiness& io_readiness) const
client->m_recv_buffer.end(),
buf,
buf + nrecv);
- // Process as much received data as we can
- MaybeDispatchRequestsFromClient(client);
}
}
+ // Process as much received data as we can.
+ // This executes for every client whether or not reading or writing
+ // took place because it also (might) parse a request we have already
+ // received and pass it to a worker thread.
+ MaybeDispatchRequestsFromClient(client);
}
}
@@ -1410,8 +1416,20 @@ void HTTPServer::MaybeDispatchRequestsFromClient(const std::shared_ptr<HTTPRemot
client->m_origin,
client->m_id);
- // handle request
- m_request_dispatcher(std::move(req));
+ // add request to client queue
+ client->m_req_queue.push_back(std::move(req));
+ }
+
+ // If we are already handling a request from
+ // this client, do nothing. We'll check again on the next I/O
+ // loop iteration.
+ if (client->m_req_busy) return;
+
+ // Otherwise, if there is a pending request in the queue, handle it.
+ if (!client->m_req_queue.empty()) {
+ client->m_req_busy = true;
+ m_request_dispatcher(std::move(client->m_req_queue.front()));
+ client->m_req_queue.pop_front();
}
}
diff --git a/src/httpserver.h b/src/httpserver.h
index cbb9dd43..339cd45f 100644
--- a/src/httpserver.h
+++ b/src/httpserver.h
@@ -6,6 +6,7 @@
#define BITCOIN_HTTPSERVER_H
#include <atomic>
+#include <deque>
#include <functional>
#include <memory>
#include <optional>
@@ -538,6 +539,16 @@ public:
*/
std::vector<std::byte> m_recv_buffer{};
+ //! Requests from a client must be processed in the order in which
+ //! they were received, blocking on a per-client basis. We won't
+ //! process the next request in the queue if we are currently busy
+ //! handling a previous request.
+ std::deque<std::unique_ptr<HTTPRequest>> m_req_queue;
+
+ //! Set to true by the I/O thread when a request is popped off
+ //! and passed to a worker thread, reset to false by the worker thread.
+ std::atomic_bool m_req_busy{false};
+
/**
* Response data destined for this client.
* Written to by http worker threads, read and erased by HTTPServer I/O thread
Why this scored 46/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.