http: Make class fields private and make HTTPResponse a struct #35829

pull hodlinator wants to merge 10 commits into bitcoin:master from hodlinator:pr/35182_suggestions changing 7 files +386 −384
  1. hodlinator commented at 11:49 AM on July 28, 2026: contributor

    The new HTTP server implementation in v32 has HTTPServer reaching into and modifying fields of HTTPRemoteClient and HTTPRequest. This PR encapsulates field data of the latter 2 types which enforces invariants and reduces cognitive load[^1]. Exposing data through accessor methods also implies adding lock annotations.

    Commits:

    Follow-up to #35182.

    [^1]: Core Guidelines: C.9: Minimize exposure of members - https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c9-minimize-exposure-of-members [^2]: Core Guidelines: C.2: Use class if the class has an invariant; use struct if the data members can vary independently - https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c2-use-class-if-the-class-has-an-invariant-use-struct-if-the-data-members-can-vary-independently

  2. DrahtBot added the label RPC/REST/ZMQ on Jul 28, 2026
  3. DrahtBot commented at 11:49 AM on July 28, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35829.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK achow101, janb84, winterrdog

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35780 (http: linger-close after parse errors so clients can read the reply by b-l-u-e)
    • #29700 (kernel, refactor: return error status on all fatal errors by ryanofsky)
    • #26022 (Add util::ResultPtr class by ryanofsky)
    • #25722 (refactor: Use util::Result class for wallet loading by ryanofsky)
    • #25665 (refactor: Add util::Result failure types and ability to merge result values by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. pinheadmz commented at 7:51 PM on July 28, 2026: member

    Light code review, changes all look fine. It's all code style cleanups (thanks 😬) though without any obvious gain. Will review more thoroughly after more concept a-c-ks

  5. DrahtBot added the label Needs rebase on Jul 29, 2026
  6. hodlinator force-pushed on Aug 10, 2026
  7. DrahtBot removed the label Needs rebase on Aug 10, 2026
  8. hodlinator commented at 1:05 PM on August 10, 2026: contributor

    Thanks for the interest @pinheadmz!

    I've updated the PR description to include more of a "Why-preamble". + Resolved conflicts with master.

  9. fanquake added this to the milestone 32.0 on Aug 17, 2026
  10. fanquake commented at 9:20 AM on August 17, 2026: member

    Can you rebase? Added to the 32 milestone, because it'd be good to get all these sort of "cleanups" done in the same release cycle.

  11. DrahtBot added the label Needs rebase on Aug 17, 2026
  12. pinheadmz commented at 11:03 AM on August 17, 2026: member

    Wanna add this too (make m_body private) #35735 (review) when you rebase since #35735 was merged

  13. hodlinator force-pushed on Aug 17, 2026
  14. DrahtBot removed the label Needs rebase on Aug 17, 2026
  15. hodlinator force-pushed on Aug 17, 2026
  16. DrahtBot added the label CI failed on Aug 17, 2026
  17. hodlinator commented at 9:33 PM on August 17, 2026: contributor

    Rebased and worked on it a bit more. The new HTTPRequest state and test from #35735 took some time to resolve. Agree with the sentiment from #35829 (comment) that it would be nice to include in the initial release of the new HTTP code.

  18. DrahtBot removed the label CI failed on Aug 17, 2026
  19. hodlinator renamed this:
    http: Make class fields private or convert to struct
    http: Make class fields private and make HTTPResponse a struct
    on Aug 18, 2026
  20. hodlinator force-pushed on Aug 18, 2026
  21. sedited requested review from pinheadmz on Aug 18, 2026
  22. winterrdog commented at 8:42 AM on August 19, 2026: contributor

    concept ACK

  23. DrahtBot added the label Needs rebase on Aug 20, 2026
  24. refactor: Make HTTPResponse a struct since all fields are public e5be0dc35e
  25. hodlinator force-pushed on Aug 20, 2026
  26. DrahtBot removed the label Needs rebase on Aug 20, 2026
  27. in src/httpserver.cpp:1117 in 25ba947fa8


    winterrdog commented at 11:57 PM on August 20, 2026:

    25ba947 refactor: Simplify boolean logic in HTTPServer::DisconnectClients():

    while at this, how about if we simplified this a bit by reducing the branching like so ?

    diff --git a/src/httpserver.cpp b/src/httpserver.cpp
    index d7d56a2a77..bad3f5e24d 100644
    --- a/src/httpserver.cpp
    +++ b/src/httpserver.cpp
    @@ -1122,23 +1122,21 @@ bool HTTPRemoteClient::MaybeDisconnect(std::chrono::time_point<SteadyClock> now,
                     m_id);
        } else if (!m_disconnect) {
            // Disconnect this client because the server is shutting
            // down and we need to disconnect all clients...
    -        if (disconnect_all) {
    -            // ...unless we still have data for this client.
    -            if (m_connection_busy) {
    -                // There is still data for this healthy-connected client.
    -                // Continue the I/O loop until all data is sent or an error is encountered.
    -                return false;
    -            } else {
    -                // This is a healthy persistent connection (e.g. keep-alive)
    -                // but it's time to say goodbye.
    -                ;
    -            }
    -        } else {
    +        if (!disconnect_all) {
                // No reason to disconnect.
                return false;
            }
    +        // ...unless we still have data for this client.
    +        if (m_connection_busy) {
    +            // There is still data for this healthy-connected client.
    +            // Continue the I/O loop until all data is sent or an error is encountered.
    +            return false;
    +        }
    +
    +        // This is a healthy persistent connection (e.g. keep-alive)
    +        // but it's time to say goodbye.
        }
        // No reason NOT to disconnect, log and remove.
        LogDebug(BCLog::HTTP,
                 "Disconnecting HTTP client %s (id=%llu)",
    

    the logic and return paths stay the same, but this gets rid of the nested if/else and makes the two return false cases more direct


    hodlinator commented at 9:58 AM on August 24, 2026:

    Decided to defer my control-flow change in previous pushes to another PR for now. If the PR doesn't make it into the release I might bring it back together with some variation of this suggestion.


    janb84 commented at 7:15 AM on August 26, 2026:

    NIT, non-blocking: by adding a friend class you can move the SetState to private, limiting outside access to the state machine even further.

    diff --git a/src/httpserver.h b/src/httpserver.h
    index 9a41838102..7c2b85e4af 100644
    --- a/src/httpserver.h
    +++ b/src/httpserver.h
    @@ -147,6 +147,8 @@ class HTTPRemoteClient;
     
     class HTTPRequest
     {
    +    friend class HTTPRemoteClient;
    +
     public:
         explicit HTTPRequest(const std::shared_ptr<HTTPRemoteClient>& client) : m_client{client} {}
         //! Construct with a null client for unit tests
    @@ -195,9 +197,10 @@ public:
             Error
         };
         State GetState() const { return m_state; }
    -    void SetState(State state) { m_state = state; }
     
     private:
    +    void SetState(State state) { m_state = state; }
    +
         HTTPRequestMethod m_method;
         std::string m_target;
         HTTPVersion m_version;
    

    hodlinator commented at 8:42 AM on August 26, 2026:

    Thanks for pointing out that HTTPRemoteClient is the only one which needs that modifier!

    Hesitant to give full friend access though. Maybe some kind of passkey pattern could be explored (https://github.com/bitcoin/bitcoin/pull/35301#discussion_r3820529374 https://chromium.googlesource.com/chromium/src.git/+/master/docs/patterns/passkey.md).

    Alternatively one could friend the specific method friend void HTTPRemoteClient::ReadRequest(HTTPRequest&);, but that involves solving problems with changing declaration-order of classes.

    Or one could remove HTTPRequest::SetState() in favor of extracting most of HTTPRemoteClient::ReadRequest() into a new HTTPRequest::Load(util::LineReader& reader).

    <details><summary>draft diff</summary>

    diff --git a/src/httpserver.cpp b/src/httpserver.cpp
    index 0d93fd34cb..bb8a382f2a 100644
    --- a/src/httpserver.cpp
    +++ b/src/httpserver.cpp
    @@ -1163,42 +1163,48 @@ void HTTPRemoteClient::ReadRequest(HTTPRequest& req)
         if (m_recv_buffer.empty()) return;
     
         LineReader reader(m_recv_buffer, MAX_HEADERS_SIZE);
    +    try {
    +        req.Load(reader);
    +    } catch (...) {
    +        // Clear the memory allocated to this client, caller must disconnect
    +        m_recv_buffer.clear();
    +        throw;
    +    }
    +
    +    // Remove the bytes read out of the buffer.
    +    m_recv_buffer.erase(
    +        m_recv_buffer.begin(),
    +        m_recv_buffer.begin() + reader.Consumed());
    +}
     
    +bool HTTPRequest::Load(util::LineReader& reader)
    +{
         try {
    -        switch (req.GetState()) {
    +        switch (m_state) {
             case HTTPRequest::State::Init:
    -            if (!req.LoadControlData(reader)) break;
    -            req.SetState(HTTPRequest::State::NeedsHeaders);
    +            if (!LoadControlData(reader)) return false;
    +            m_state = HTTPRequest::State::NeedsHeaders;
                 [[fallthrough]];
     
             case HTTPRequest::State::NeedsHeaders:
    -            if (!req.LoadHeaders(reader)) break;
    -            req.SetState(HTTPRequest::State::NeedsBody);
    +            if (!LoadHeaders(reader)) return false;
    +            m_state = HTTPRequest::State::NeedsBody;
                 [[fallthrough]];
     
             case HTTPRequest::State::NeedsBody:
    -            if (!req.LoadBody(reader)) break;
    -            req.SetState(HTTPRequest::State::Complete);
    +            if (!LoadBody(reader)) return false;
    +            m_state = HTTPRequest::State::Complete;
                 [[fallthrough]];
     
             case HTTPRequest::State::Complete:
    -            break;
    -
             case HTTPRequest::State::Error:
    -            break;
    +            return true;
             }
         } catch (...) {
             // Don't try to read any more data for this request
    -        req.SetState(HTTPRequest::State::Error);
    -        // Clear the memory allocated to this client, caller must disconnect
    -        m_recv_buffer.clear();
    +        m_state = HTTPRequest::State::Error;
             throw;
         }
    -
    -    // Remove the bytes read out of the buffer.
    -    m_recv_buffer.erase(
    -        m_recv_buffer.begin(),
    -        m_recv_buffer.begin() + reader.Consumed());
     }
     
     bool HTTPRemoteClient::MaybeSendBytesFromBuffer()
    diff --git a/src/httpserver.h b/src/httpserver.h
    index 9a41838102..8f19437627 100644
    --- a/src/httpserver.h
    +++ b/src/httpserver.h
    @@ -153,18 +153,11 @@ public:
         explicit HTTPRequest() : m_client{} {}
     
         /**
    -     * Methods that attempt to parse HTTP request fields line-by-line
    -     * from a receive buffer.
    -     * [@param](/bitcoin-bitcoin/contributor/param/)[in]   reader  A LineReader object constructed over a span of data.
    -     * [@returns](/bitcoin-bitcoin/contributor/returns/)     true    If the request field was parsed.
    -     *              false   If there was not enough data in the buffer to complete the field.
    +     * [@returns](/bitcoin-bitcoin/contributor/returns/)     true    If the request was fully parsed or in error.
    +     *              false   If there was not enough data in the buffer.
          * [@throws](/bitcoin-bitcoin/contributor/throws/)      std::runtime_error if data is invalid.
          */
    -    /// @{
    -    bool LoadControlData(util::LineReader& reader);
    -    bool LoadHeaders(util::LineReader& reader);
    -    bool LoadBody(util::LineReader& reader);
    -    /// @}
    +    bool Load(util::LineReader& reader);
     
         void WriteReply(HTTPStatusCode status, std::span<const std::byte> reply_body = {});
         void WriteReply(HTTPStatusCode status, std::string_view reply_body_view)
    @@ -195,9 +188,22 @@ public:
             Error
         };
         State GetState() const { return m_state; }
    -    void SetState(State state) { m_state = state; }
     
     private:
    +    /**
    +     * Methods that attempt to parse HTTP request fields line-by-line
    +     * from a receive buffer.
    +     * [@param](/bitcoin-bitcoin/contributor/param/)[in]   reader  A LineReader object constructed over a span of data.
    +     * [@returns](/bitcoin-bitcoin/contributor/returns/)     true    If the request field was parsed.
    +     *              false   If there was not enough data in the buffer to complete the field.
    +     * [@throws](/bitcoin-bitcoin/contributor/throws/)      std::runtime_error if data is invalid.
    +     */
    +    /// @{
    +    bool LoadControlData(util::LineReader& reader);
    +    bool LoadHeaders(util::LineReader& reader);
    +    bool LoadBody(util::LineReader& reader);
    +    /// @}
    +
         HTTPRequestMethod m_method;
         std::string m_target;
         HTTPVersion m_version;
    diff --git a/src/test/fuzz/http_request.cpp b/src/test/fuzz/http_request.cpp
    index c24413add0..5314339a92 100644
    --- a/src/test/fuzz/http_request.cpp
    +++ b/src/test/fuzz/http_request.cpp
    @@ -29,9 +29,7 @@ FUZZ_TARGET(http_request)
         HTTPRequest http_request;
         LineReader reader(http_buffer, MAX_HEADERS_SIZE);
         try {
    -        if (!http_request.LoadControlData(reader)) return;
    -        if (!http_request.LoadHeaders(reader)) return;
    -        if (!http_request.LoadBody(reader)) return;
    +        http_request.Load(reader);
         } catch (const std::runtime_error&) {
             return;
         }
    diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp
    index f853a2d01c..8364aa641e 100644
    --- a/src/test/httpserver_tests.cpp
    +++ b/src/test/httpserver_tests.cpp
    @@ -196,9 +196,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
         {
             HTTPRequest req;
             LineReader reader(full_request, MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK(req.LoadBody(reader));
    +        BOOST_CHECK(req.Load(reader));
             BOOST_CHECK_EQUAL(req.GetRequestMethod(), HTTPRequestMethod::POST);
             BOOST_CHECK_EQUAL(req.GetURI(), "/");
             BOOST_CHECK_EQUAL(req.GetVersion().major, 1);
    @@ -214,61 +212,61 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
             // Malformed: no spaces between data
             HTTPRequest req;
             LineReader reader("GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP request line too short"});
         }
         {
             // Malformed: too many spaces
             HTTPRequest req;
             LineReader reader("GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
         }
         {
             // Malformed: slash missing before version
             HTTPRequest req;
             LineReader reader("GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP request line too short"});
         }
         {
             // Malformed: no decimal in version
             HTTPRequest req;
             LineReader reader("GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP request line too short"});
         }
         {
             // Malformed: version is not a number
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
         }
         {
             // Malformed: version is out of range
             HTTPRequest req;
             LineReader reader("GET / HTTP/2.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
         }
         {
             // Malformed: version is out of range
             HTTPRequest req;
             LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
         }
         {
             // Malformed: version is out of range
             HTTPRequest req;
             LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
         }
         {
             // Malformed: version is not exactly two integers and a dot
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP bad version"});
         }
         {
             // Malformed: contains NUL
             HTTPRequest req;
             LineReader reader{std::string_view{"GET /safe\0/etc/passwd HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", 50}, MAX_HEADERS_SIZE};
    -        BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
         }
         {
             // Malformed: differing Content-Length values, case insensitive
    @@ -279,9 +277,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
                                                           "12345678";
             HTTPRequest req;
             util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
         }
         {
             // Ok: multiple same Content-Length values
    @@ -292,17 +288,13 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
                                                           "12345678";
             HTTPRequest req;
             util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK(req.LoadBody(reader));
    +        BOOST_CHECK(req.Load(reader));
         }
         {
             // Ok
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK(req.LoadBody(reader));
    +        BOOST_CHECK(req.Load(reader));
             BOOST_CHECK_EQUAL(req.GetRequestMethod(), HTTPRequestMethod::GET);
             BOOST_CHECK_EQUAL(req.GetURI(), "/");
             BOOST_CHECK_EQUAL(req.GetVersion().major, 1);
    @@ -315,8 +307,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
             // Malformed: missing colon
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK_EXCEPTION(req.LoadHeaders(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
         }
         {
             // We might not have received enough data from the client which is not
    @@ -324,16 +315,13 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
             // buffer has more data.
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.0\r\nHost: ", MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(!req.LoadHeaders(reader));
    +        BOOST_CHECK(!req.Load(reader));
         }
         {
             // No Content-Length: body is not read
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.0\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK(req.LoadBody(reader));
    +        BOOST_CHECK(req.Load(reader));
             // Don't try to read request body if Content-Length is missing
             BOOST_CHECK_EQUAL(req.ReadBody(), "");
         }
    @@ -341,17 +329,13 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
             // Malformed: Content-Length is not a number
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
         }
         {
             // Malformed: Content-Length is negative
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.0\r\nContent-Length: -8\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
         }
         {
             // Content-Length exceeds limit
    @@ -360,9 +344,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
             const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(excessive_size) + "\r\n\r\n" + std::move(huge_body)};
             HTTPRequest req;
             LineReader reader(request, MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), ContentTooLargeError, HasReason{"Max body size exceeded"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), ContentTooLargeError, HasReason{"Max body size exceeded"});
         }
         {
             // Content-Length exactly on the limit
    @@ -370,18 +352,14 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
             const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(MAX_BODY_SIZE) + "\r\n\r\n" + std::move(max_body)};
             HTTPRequest req;
             LineReader reader(request, MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK(req.LoadBody(reader));
    +        BOOST_CHECK(req.Load(reader));
         }
         {
             // Content-Length indicates more data than we have in the buffer.
             // Not an error; we wait for more data before completing the body.
             HTTPRequest req;
             LineReader reader("GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK(!req.LoadBody(reader));
    +        BOOST_CHECK(!req.Load(reader));
         }
         {
             // Support "chunked" transfer. Chunk lengths are ascii-encoded hex integers, whitespace ignored
    @@ -396,9 +374,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
                                           "0\n"
                                           "\n";
             LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK(req.LoadBody(reader));
    +        BOOST_CHECK(req.Load(reader));
             BOOST_CHECK_EQUAL(req.ReadBody(), R"({"method":"getblockcount"})");
         }
         {
    @@ -414,9 +390,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
                                                     "0\n"
                                                     "\n";
             LineReader reader(excessive_chunk_size, MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), ContentTooLargeError, HasReason{"Chunk will exceed max body size"});
         }
         {
             // Allow (but ignore) Chunk Extensions
    @@ -432,9 +406,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
                                           "Expires: Wed, 21 Oct 2026 07:28:00 GMT\n"
                                           "\n";
             LineReader reader(ok_chunked, MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK(req.LoadBody(reader));
    +        BOOST_CHECK(req.Load(reader));
             BOOST_CHECK_EQUAL(req.ReadBody(), R"({"method":"getblockcount"})");
             // Chunk Trailer was parsed, but ignored
             BOOST_CHECK_EQUAL(reader.Remaining(), 0);
    @@ -453,9 +425,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
                                                "0\n"
                                                "\n";
             LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Cannot parse chunk length value"});
         }
         {
             // Invalid "chunked" transfer, missing chunk termination \n
    @@ -470,9 +440,7 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
                                                "0\n"
                                                "\n";
             LineReader reader(invalid_chunked, MAX_HEADERS_SIZE);
    -        BOOST_CHECK(req.LoadControlData(reader));
    -        BOOST_CHECK(req.LoadHeaders(reader));
    -        BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Improperly terminated chunk"});
    +        BOOST_CHECK_EXCEPTION(req.Load(reader), std::runtime_error, HasReason{"Improperly terminated chunk"});
         }
     }
     
    

    </details>

    Deferring for now.


    janb84 commented at 8:48 AM on August 26, 2026:

    I get the hesitation for the full friend access. I just wanted to point it out ;)


    hodlinator commented at 8:06 AM on August 31, 2026:

    Took a stab at making request state updates internal to the request type as part of #36124. Thanks again for pointing this out!


    hodlinator commented at 8:09 AM on August 31, 2026:

    A commit flattening out the control flow even further is now included in #36124.

  28. in src/httpserver.h:156 in 99de5d0715 outdated
     146 | @@ -153,7 +147,6 @@ class HTTPRemoteClient;
     147 |  
     148 |  class HTTPRequest
     149 |  {
     150 | -public:
    


    winterrdog commented at 12:06 AM on August 21, 2026:

    nit: HTTPRequest and HTTPRemoteClient drop the explicit private: label and just rely on the class default but most other classes here (HTTPHeaders, HTTPServer and most classes across the codebase) spell out both sections even when it is redundant

    intentional, or it can be added back for consistency ?


    hodlinator commented at 9:53 AM on August 24, 2026:

    Omitting initial private: is a personal preference. Decided instead to switch to conventional class member declaration order instead as pointed out by #35829 (comment).

  29. in src/httpserver.h:489 in 99de5d0715 outdated
     474 | @@ -486,7 +475,6 @@ std::optional<std::string> GetQueryParameterFromUri(std::string_view uri, std::s
     475 |  
     476 |  class HTTPRemoteClient
     477 |  {
     478 | -public:
    


    winterrdog commented at 12:09 AM on August 21, 2026:

    nit: same as this comment

  30. purpleKarrot commented at 3:49 AM on August 21, 2026: contributor

    adhere to common C++ practice such as described in Core Guidelines

    You might want to remove that argument to avoid risking a N-A-C-K, as some reviewers are allergic to appeals to authority (see #35569 (review)). I fell into the same trap and received push back.

    But seriously, regarding

    without any obvious gain (https://github.com/bitcoin/bitcoin/pull/35829#issuecomment-5108965448)

    Encapsulation is an obvious gain!

    drop the explicit private: label (https://github.com/bitcoin/bitcoin/pull/35829#discussion_r3826288202)

    If you want to follow "common C++ practice such as described in Core Guidelines", then you might want to follow guideline NL.16 as well.

  31. in src/httpserver.h:173 in 99de5d0715 outdated
     168 | +    uint64_t m_chunk_read{0};
     169 | +
     170 | +public:
     171 |      explicit HTTPRequest(const std::shared_ptr<HTTPRemoteClient>& client) : m_client{client} {}
     172 |      //! Construct with a null client for unit tests
     173 |      explicit HTTPRequest() : m_client{} {}
    


    janb84 commented at 10:07 AM on August 21, 2026:

    NIT, slightly stricter, imho better with the more private members.

        explicit HTTPRequest() = default; 
    

    hodlinator commented at 9:52 AM on August 24, 2026:

    Agree, but deferring for now as it's slightly orthogonal.


    hodlinator commented at 8:08 AM on August 31, 2026:

    Left out the defaulted test constructor suggestion from #36124 as despite agreeing initially I'm not sure it's significant enough to motivate code churn/review.

  32. janb84 commented at 10:08 AM on August 21, 2026: contributor

    concept ACK 99de5d07153da2e0789f1419ec38b300850c60fc

  33. refactor: Make HTTPRequest::GetHeader() return saner optional type
    No need to stick to weird old API from libevent-wrapper days.
    
    Makes later commits in the PR cleaner.
    b8cd77237b
  34. refactor: Make HTTPRequest fields private
    Makes sense since they are only set by methods in the class itself, and already had accessors for most fields.
    6fec8d6914
  35. refactor: Extract HTTPRemoteClient::MaybeDisconnect() from HTTPServer::DisconnectClients() 6d9b61d4f8
  36. refactor: Extract Send() and Receive() into HTTPRemoteClient from HTTPServer a1183c02aa
  37. hodlinator force-pushed on Aug 24, 2026
  38. hodlinator commented at 10:04 AM on August 24, 2026: contributor

    Latest push drops the more orthogonal changes and focuses on what's in the PR title in the hope of increasing chances for being included in v32.

  39. in src/httpserver.h:502 in a6dc7f7bda
     497 | +     * Try to read HTTPRequests from a client's receive buffer.
     498 | +     * Complete requests are returned, incomplete requests are
     499 | +     * left in the buffer to wait for more data. Some read errors
     500 | +     * will mark this client for disconnection.
     501 | +     */
     502 | +    static std::unique_ptr<HTTPRequest> ReadRequests(const std::shared_ptr<HTTPRemoteClient>& client);
    


    winterrdog commented at 6:31 PM on August 24, 2026:

    just wondering about ReadRequests() here: since it does not loop over the m_recv_buffer buffer (changed by #35735) or return multiple requests, and instead just tries to read the current request and returns it once complete, otherwise it returns a nullptr, was the plural meant to convey something i am missing?

    would ReadRequest() or TryReadRequest() be a better fit ?


    the spark:

    the naming is a remnant from #35182 when we still used a request queue: https://github.com/bitcoin/bitcoin/blob/f595daf1dd01e9730e0eafbc46b2e22eeb9f33fe/src/httpserver.cpp#L984-L1043

    currently, we process one request from a client in serial. from #35735 's PR description:

    Only parse one request at a time from the receive buffer. The server processes requests from each client in series anyway.


    hodlinator commented at 9:21 PM on August 24, 2026:

    Agreed, TryReadRequest() matches better what it's currently doing since it's no longer eagerly parsing and queuing up later request objects.

  40. achow101 commented at 8:55 PM on August 24, 2026: member

    ACK a6dc7f7bda97b1d9bc23fdb8e902d78f0bb6befc

    Since we are past the 32.0 feature freeze, I'm wondering what about this refactor makes it a candidate for 32.0? Generally, we only merge bug fixes after feature freeze, and I'm not sure that this meets that bar.

  41. DrahtBot requested review from winterrdog on Aug 24, 2026
  42. DrahtBot requested review from janb84 on Aug 24, 2026
  43. fanquake commented at 9:20 AM on August 25, 2026: member

    Given these are improvements/cleanups to a major new feature, I think it makes sense to merge in the same release cycle, rather than after branch off, and then making it harder to backports any bug fixes later.

  44. hodlinator force-pushed on Aug 25, 2026
  45. hodlinator commented at 9:42 AM on August 25, 2026: contributor

    Latest push only improves the naming of HTTPRemoteClients ReadRequests() - now it's TryReadRequest(). (ReadRequests() was already a new name in this PR). Comment for the method was also improved.

    git range-diff a6dc7f7b...d551fc71
    
  46. refactor: Replace HTTPServer::MaybeDispatchRequestsFromClient() with HTTPRemoteClient::TryReadRequest() 5b06d90831
  47. refactor: Expose HTTPRemoteClient fields to tests through methods
    Enables making the fields private later.
    10bbae302f
  48. refactor: Expose additional HTTPRemoteClient fields through accessors d72f67fd6c
  49. refactor: Make HTTPRemoteClient fields private
    Move-only change.
    
    Also makes ReadRequest() private.
    8f9fd8698a
  50. refactor: Drastically narrow scope of http_bitcoin namespace and rename it to bitcoin_http
    http_bitcoin was mostly used during #35182 to distinguish from http_libevent counterpart:
    - The http_libevent namespace was introduced around the legacy code in 89c54ae4cbc8e58921551d5f1a90eb4683106ccb.
    - The http_bitcoin namespace was introduced in 68b5d289d19c42de9bebf54a0555053d29721111 and extended in subsequent commits.
    - The http_libevent namespace together with code it contained was removed in 8c1eea0777c586ce58a500bbea509cd39e4f3507.
    
    bitcoin_http is a better name as it is Bitcoin Core's implementation of the HTTP protocol, not HTTP protocol's implementation of bitcoin 402 payment required codes or anything like that.
    The namespace only remains for a few constants and a type which don't have HTTP in their names.
    5e0d7a286a
  51. in src/httpserver.h:497 in d551fc7186
     492 | +    void Receive() EXCLUSIVE_LOCKS_REQUIRED(!m_sock_mutex);
     493 | +
     494 | +    bool MaybeDisconnect(std::chrono::time_point<SteadyClock> now, std::chrono::seconds rpcservertimeout, bool disconnect_all);
     495 | +
     496 | +    /**
     497 | +     * Try to read a HTTPRequest from a client's receive buffer.
    


    janb84 commented at 11:11 AM on August 25, 2026:

    In the case that you re-touch this PR, please change this typo

         * Try to read an HTTPRequest from a client's receive buffer.
    
  52. hodlinator force-pushed on Aug 25, 2026
  53. hodlinator commented at 11:26 AM on August 25, 2026: contributor

    (Pushed 1-char typo fix: #35829 (review))

  54. achow101 commented at 8:47 PM on August 25, 2026: member

    ACK 5e0d7a286a49d14018068bd413833ffaef6af37e

  55. DrahtBot requested review from janb84 on Aug 25, 2026
  56. janb84 commented at 7:26 AM on August 26, 2026: contributor

    ACK 5e0d7a286a49d14018068bd413833ffaef6af37e

    Build tested and did a code review, found one non-blocking nit (see below).

    This PR cleans-up the code (make class field private) and adds some minor improvement to the test coverage (where I was wondering if we didn't lost some coverage). This PR, to me, is an improvement to code quality, LGTM !

  57. winterrdog commented at 9:53 AM on August 26, 2026: contributor

    tACK 5e0d7a286a49d14018068bd413833ffaef6af37e

    successfully built and tested on Debian/x86_64/clang++-18


    small nit; in the PR description:

    i think this can be removed based on the agreement we arrived at here: #35829 (review)

  58. fanquake merged this on Aug 26, 2026
  59. fanquake closed this on Aug 26, 2026

  60. pinheadmz commented at 5:55 PM on August 27, 2026: member

    post-merge ACK 5e0d7a286a49d14018068bd413833ffaef6af37e

    Thanks @hodlinator and @janb84 for the encapsulation and overall clean up. I built and tested on macos/arm64 and reviewed all changes mainly so I won't repeat the same patterns in future work!

    <details><summary>Show Signature</summary>

    -----BEGIN PGP SIGNED MESSAGE-----
    Hash: SHA256
    
    ACK 5e0d7a286a49d14018068bd413833ffaef6af37e
    -----BEGIN PGP SIGNATURE-----
    
    iQJPBAEBCAA5FiEE5hdzzW4BBA4vG9eM5+KYS2KJyToFAmqQebIbFIAAAAAABAAO
    bWFudTIsMi41KzEuMTIsMCwzAAoJEOfimEtiick6TPwQALoIekM0RcbGiDzGhlfs
    u7s6QgMm9w9BL993nDnWLPVoQ5jHn7RALAy5z0iXqa7kMAeHaQXI+PsK0h9AWjUK
    4B/AGroNZnnbp6vweBmLhF5bDjsowEdmWm8BdGdbMKP0nGQw+utIII6oXGDr7y8v
    Kb7w3LjpC2Egme6FsVb7hnuOmgG8epbSGWJbOE5bJYum5Hhu45vEno2t+iLOgjhI
    joLHx/fpZEgf6I5kPYt+4TnUH6YgaTCEK7a9TwHDMmrfalGnkI8uDO1HUbK8WQ23
    tyk73cjVszb5p3E20J3GpC8H9RGTbSvOq+61qmsgqtgPq0XkiLcyVUozl4fW/54s
    xUpaiwPGnaRbCuy0TDOx5nzQ2iMuwfMeX0P1YG3q6hbKiq22B/+qY2vK9Ix9sJjx
    d3eMgz6Y1LMGDlXfJiunXgmCOfflgYR4A15lMj9o3gOC/A7WoRN3AA2PZnx9s7yX
    stUjtlM47eXF0C4cqY2tJHlOBNBMS4pkLsOI59sXXAEhO5Z1ARTtPBRdXDAV6Vmk
    2QEwZGdD4xM8EUHz2OTJLtZu0pEX7r2qjfHz0NoMe8vx2/HEOIDqxroPO3frWoSU
    /ccfaDx2EnQ5FOxjviBBSxsaQ5usNlhvZMvzuHrk/5Dqy3JHJ7CoSdUhmZUjtlXA
    s0NQReWxWvg+Ip0axhfSHjGB
    =hjqZ
    -----END PGP SIGNATURE-----
    

    pinheadmz's public key is on openpgp.org

    </details>

  61. hodlinator deleted the branch on Aug 27, 2026
  62. Kino1994 referenced this in commit 7789890df1 on Aug 31, 2026
  63. hodlinator commented at 8:25 AM on August 31, 2026: contributor

    re: #35829 (comment)

    #35182 was such a massive undertaking with broad coverage, very extensive review and multiple approaches to get it in. @pinheadmz is the metaphorical "man in the arena" and I hope he doesn't see my follow-up work as trivial critiquing or taking away from that accomplishment. They are tiny in comparison but I think they still make the code easier to work with going forward.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-08 12:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me