{
 "number": 36124,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36124",
 "title": "http: Make `HTTPRequest` update state internally",
 "author": "hodlinator",
 "author_association": "MEMBER",
 "created_at": "2026-08-31T07:58:53Z",
 "updated_at": "2026-09-16T17:51:51Z",
 "age_days": 17,
 "draft": false,
 "labels": [
  "RPC/REST/ZMQ"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "08d0347acb812ced579f782e4a2daf2c4d81546e",
 "head_ref": "2026/08/http_refactors",
 "head_repo": "hodlinator/bitcoin",
 "head_history": [
  {
   "t": "2026-09-02T10:09:31Z",
   "sha": "907d7342b60dfa00ad81e235eccb5808b9c13da6"
  },
  {
   "t": "2026-09-03T09:02:39Z",
   "sha": "0ac3f045e4bca1c6552f50827e9f7e52a9ea47ed"
  },
  {
   "t": "2026-09-03T09:04:40Z",
   "sha": "79fe316d30919fc4dec0a1f26958a31db94160b3"
  },
  {
   "t": "2026-09-03T09:25:49Z",
   "sha": "6bd36818e07715a2df863d0ec7f3fd54d0a4f135"
  },
  {
   "t": "2026-09-03T10:41:53Z",
   "sha": "c43d544b16b61e3182467dba7355cdaa372204c6"
  },
  {
   "t": "2026-09-05T20:52:20Z",
   "sha": "62796e659fb79a0c6f1749677ec480a80ec26e6d"
  },
  {
   "t": "2026-09-10T18:14:23Z",
   "sha": "92045f9bf6585690708b16a33b67792f50c74016"
  },
  {
   "t": "2026-09-16T16:28:57Z",
   "sha": "08d0347acb812ced579f782e4a2daf2c4d81546e"
  }
 ],
 "additions": 117,
 "deletions": 144,
 "changed_files": 4,
 "commit_count": 3,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/36124#pullrequestreview-5070443277"
     }
    ],
    "stale_ack": [
     {
      "login": "janb84",
      "url": "https://github.com/bitcoin/bitcoin/pull/36124#pullrequestreview-5102409506"
     },
     {
      "login": "winterrdog",
      "url": "https://github.com/bitcoin/bitcoin/pull/36124#issuecomment-5554882234"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36204,
     "title": "http: disconnect clients that never finish a request",
     "author": "janb84"
    },
    {
     "number": 36160,
     "title": "refactor: Minor improvements to HTTP unit tests",
     "author": "hodlinator"
    },
    {
     "number": 36135,
     "title": "fuzz: test HTTPRequest state machine in http_request",
     "author": "frankomosh"
    }
   ]
  }
 },
 "acks_parsed": {
  "jeanpablojp": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-31T19:38:51Z",
   "stale": false
  },
  "winterrdog": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-01T09:21:31Z",
   "stale": false
  },
  "janb84": {
   "kind": "concept_ack",
   "hash": "907d7342b60dfa00ad81e235eccb5808b9c13da6",
   "t": "2026-09-02T15:08:24Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 3,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "janb84",
   "jeanpablojp",
   "winterrdog"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-16T16:28:57Z",
  "last_reviewer_activity": "2026-09-05T21:24:33Z",
  "last_reviewer": "winterrdog",
  "author_silent_days": 0,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   35829,
   36159,
   36160
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35829,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-08-26",
    "title": "http: Make class fields private and make HTTPResponse a struct"
   },
   {
    "number": 36159,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "http: Improve HTTPRemoteClient::MaybeDisconnect()"
   },
   {
    "number": 36160,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-16",
    "title": "refactor: Minor improvements to HTTP unit tests"
   }
  ],
  "conflicts": [
   36204,
   36160,
   36135
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/httpserver.cpp",
  "src/httpserver.h",
  "src/test/fuzz/http_request.cpp",
  "src/test/httpserver_tests.cpp"
 ],
 "body": "### Issues in order of importance\n\n#35829 made the fields of `HTTPRequest` & `HTTPRemoteClient` private, but still allows anyone with a reference to a request to update the internal state `enum`. https://github.com/bitcoin/bitcoin/pull/35829#discussion_r3860447518\n\n`HTTPRemoteClient::TryReadRequest()` only needs to be a `static` method in order to forward a `shared_ptr` of a client to a new request.\n\n### Commits solving these issues in same order\n\n* http: Remove `HTTPRequest::SetState()` and introduce `HTTPRequest::Load()` - Removes the ability for other classes to directly modify the request state.\n* 2 refactors: Extract `HTTPRemoteClient::TryReadRequestInternal()` from a combination of `HTTPRemoteClient::TryReadRequest()` and `HTTPRemoteClient::ReadRequest()` (the latter is removed)",
 "commits": [
  {
   "sha": "6983eb0d4989125762bce2c0a6e9fce3494e2956",
   "date": "2026-09-16T16:24:05Z",
   "message": "http: Extract HTTPRequest::Load() to remove SetState()\n\nRemoves the ability for other classes to directly modify the request state.\n\nRefactor with slight API change as pre-existing Load*-methods are made private, which mainly affects unit tests."
  },
  {
   "sha": "ef877990eab4d19f736c324dff83c65a235ef16d",
   "date": "2026-09-16T16:24:05Z",
   "message": "refactor: Extract HTTPRemoteClient::TryReadRequestInternal() from static HTTPRemoteClient::TryReadRequest()\n\nTryReadRequest() now only remains as an outer method for the sake of passing the client shared_ptr to the new request."
  },
  {
   "sha": "08d0347acb812ced579f782e4a2daf2c4d81546e",
   "date": "2026-09-16T16:24:05Z",
   "message": "refactor: Flatten HTTPRemoteClient::ReadRequest() into HTTPRemoteClient::TryReadRequestInternal()"
  }
 ],
 "timeline": [
  {
   "t": "2026-08-31T19:38:51Z",
   "kind": "review",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "549cbbf67f41039887e010d64d592a6b64a4b41f",
   "text": "Concept ACK\n\nI ran the flattened `ShouldDisconnect()` against the version in master over every combination of the flags it reads, with the client inside and outside the idle window and the timeout on and off. Same return value in all 64. The only difference is that master logs the idle timeout even for a client that is already flagged for disconnect."
  },
  {
   "t": "2026-08-31T19:38:51Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/httpserver_tests.cpp",
   "commit": "6bd36818e07715a2df863d0ec7f3fd54d0a4f135",
   "in_reply_to": null,
   "text": "The unit tests run `ShouldDisconnect()` but assert nothing about its first two branches. I broke each one and the suite stayed green both times. `interface_http.py` does reach both, but it blocks on the socket instead of failing. The test below covers both branches, and the new log line with them.\n\ntest for both branches and the new log line\n\n```diff\n@@ -746,6 +746,38 @@ BOOST_AUTO_TEST_CASE(http_request_state_tests)\n     }\n }\n\n+BOOST_AUTO_TEST_CASE(http_should_disconnect_tests)\n+{\n+    struct Client : HTTPRemoteClient {\n+        Client() : HTTPRemoteClient{/*id=*/0, /*addr=*/CService(), /*socket=*/CreateSock(0, 0, 0)} {}\n+        using HTTPRemoteClient::MutateRecvBuffer;\n+    };\n+    constexpr auto timeout{30s};\n+    const auto now{Now<SteadySeconds>()};\n+\n+    // Nothing to disconnect for yet.\n+    auto idle{std::make_shared<Client>()};\n+    BOOST_CHECK(!idle->ShouldDisconnect(now, timeout, /*disconnect_all=*/false));\n+    // A shutdown waits for a connection that is still busy, and it starts busy.\n+    BOOST_CHECK(!idle->ShouldDisconnect(now, timeout, /*disconnect_all=*/true));\n+    // Past -rpcservertimeout, which a timeout of 0 disables.\n+    BOOST_CHECK(idle->ShouldDisconnect(now + timeout + 1s, timeout, /*disconnect_all=*/false));\n+    BOOST_CHECK(!idle->ShouldDisconnect(now + timeout + 1s, 0s, /*disconnect_all=*/false));\n+\n+    // A request handed to a worker holds the idle timeout off until the reply is sent.\n+    auto busy{std::make_shared<Client>()};\n+    busy->MutateRecvBuffer().append(\"GET / HTTP/1.0\\n\\n\");\n+    auto request{HTTPRemoteClient::TryReadRequest(busy)};\n+    BOOST_REQUIRE(request);\n+    BOOST_CHECK(!busy->ShouldDisconnect(now + timeout + 1s, timeout, /*disconnect_all=*/false));\n+\n+    // A malformed request flags the client, and that outranks the rest.\n+    auto bad{std::make_shared<Client>()};\n+    bad->MutateRecvBuffer().append(\"GET / HTTP/1.0\\nInvalid header with no colon\\n\\n\");\n+    BOOST_CHECK(!HTTPRemoteClient::TryReadRequest(bad));\n+    BOOST_CHECK(bad->ShouldDisconnect(now, 0s, /*disconnect_all=*/false));\n+}\n+\n BOOST_AUTO_TEST_CASE(http_server_socket_tests)\n {\n     // Hard code the timestamp for the Date header in the HTTP response\n@@ -791,6 +823,9 @@ BOOST_AUTO_TEST_CASE(http_server_socket_tests)\n     // Create a mock client with pre-loaded request data and add it to the local CreateSock queue.\n     // Keep a handle for the mock client's send and receive pipes so we can examine\n     // the data it \"receives\".\n+    // No keep-alive, so the server closes once the reply is flushed, and says so.\n+    DebugLogHelper find_close{\"Done sending to client without keep-alive\"};\n+\n     std::shared_ptr<DynSock::Pipes> mock_client_socket_pipes{ConnectClient(std::as_bytes(std::span(full_request)))};\n\n     // Wait up to a minute to find and connect the client in the I/O loop\n```"
  },
  {
   "t": "2026-09-01T09:21:31Z",
   "kind": "review",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "549cbbf67f41039887e010d64d592a6b64a4b41f",
   "text": "Concept ACK"
  },
  {
   "t": "2026-09-01T15:58:17Z",
   "kind": "review_comment",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "path": "src/httpserver.cpp",
   "commit": "08d0347acb812ced579f782e4a2daf2c4d81546e",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n---\ndo you think some runtime safety like `assert(false)` (in debug builds) would be useful here ?\n\ne.g. similar to how it's done:\nhttps://github.com/bitcoin/bitcoin/blob/dc0395c5858a1d55239b82a834e5075cf2069219/src/index/blockfilterindex.cpp#L64-L71\n\nor here:\nhttps://github.com/bitcoin/bitcoin/blob/dc0395c5858a1d55239b82a834e5075cf2069219/src/bitcoin-cli.cpp#L430-L445"
  },
  {
   "t": "2026-09-02T10:07:52Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/httpserver_tests.cpp",
   "commit": "6bd36818e07715a2df863d0ec7f3fd54d0a4f135",
   "in_reply_to": 3897747671,
   "text": "Thanks! Added the checks in slightly extended form."
  },
  {
   "t": "2026-09-02T10:09:31Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "907d7342b60dfa00ad81e235eccb5808b9c13da6"
  },
  {
   "t": "2026-09-02T14:21:20Z",
   "kind": "review_comment",
   "who": "janb84",
   "assoc": "MEMBER",
   "path": "src/httpserver.cpp",
   "commit": "907d7342b60dfa00ad81e235eccb5808b9c13da6",
   "in_reply_to": null,
   "text": "NIT: would it not help the readability to move this function closer to the other load* helpers ? ~ L 378"
  },
  {
   "t": "2026-09-02T15:07:51Z",
   "kind": "review_comment",
   "who": "janb84",
   "assoc": "MEMBER",
   "path": "src/httpserver.h",
   "commit": "0ac3f045e4bca1c6552f50827e9f7e52a9ea47ed",
   "in_reply_to": null,
   "text": "NIT. On error it does not return `true`. Why not make it void? non-test code already checks `getstate()` and doing the same in the tests makes the test slightly more strict. The bool loosy communicates 5 states in a bool\n\n```suggestion\n     * @returns     true    If the request was fully parsed or already complete\n```"
  },
  {
   "t": "2026-09-02T15:08:24Z",
   "kind": "review",
   "who": "janb84",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "907d7342b60dfa00ad81e235eccb5808b9c13da6",
   "text": "concept ACK 907d7342b60dfa00ad81e235eccb5808b9c13da6\n\nThe removal of the `SetState()` makes it a better design (imho), makes the convention based \"rule\" not to arbitrary set the state, to a  a compiler-enforced fact\n\nFound a few possible NITS."
  },
  {
   "t": "2026-09-03T08:07:28Z",
   "kind": "review_comment",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/fuzz/http_request.cpp",
   "commit": "907d7342b60dfa00ad81e235eccb5808b9c13da6",
   "in_reply_to": null,
   "text": "**nit**: this comment still refers to `LoadControlData`/`LoadHeaders`/`LoadBody` as the call sequence above, but those were replaced by a single `Load()` call. since the helpers are private now too, it is not immediately clear to a reader where this logic comes from"
  },
  {
   "t": "2026-09-03T09:02:39Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "0ac3f045e4bca1c6552f50827e9f7e52a9ea47ed"
  },
  {
   "t": "2026-09-03T09:04:40Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "79fe316d30919fc4dec0a1f26958a31db94160b3"
  },
  {
   "t": "2026-09-03T09:07:06Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/httpserver.cpp",
   "commit": "907d7342b60dfa00ad81e235eccb5808b9c13da6",
   "in_reply_to": 3915112397,
   "text": "re https://github.com/bitcoin/bitcoin/pull/36124#discussion_r3915112397:\nThe order of methods in httpserver.cpp is wildly out of sync with the header. I prefer to punt on this for now, to keep the number of commits down."
  },
  {
   "t": "2026-09-03T09:08:04Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/httpserver.h",
   "commit": "0ac3f045e4bca1c6552f50827e9f7e52a9ea47ed",
   "in_reply_to": 3915573840,
   "text": "re https://github.com/bitcoin/bitcoin/pull/36124#discussion_r3915573840:\nRemoved docstring for now as I switched to returning state which seems self-explanatory."
  },
  {
   "t": "2026-09-03T09:25:49Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "6bd36818e07715a2df863d0ec7f3fd54d0a4f135"
  },
  {
   "t": "2026-09-03T09:36:58Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "6bd36818e07715a2df863d0ec7f3fd54d0a4f135",
   "text": "Thanks for the feedback so far! Pushes after 907d7342b60dfa00ad81e235eccb5808b9c13da6 address some nits and expands `http_should_disconnect_tests` to more completely verify optimistic send behavior."
  },
  {
   "t": "2026-09-03T10:41:53Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "c43d544b16b61e3182467dba7355cdaa372204c6"
  },
  {
   "t": "2026-09-03T10:44:03Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/httpserver_tests.cpp",
   "commit": "6bd36818e07715a2df863d0ec7f3fd54d0a4f135",
   "in_reply_to": 3897747671,
   "text": "Added co-authorship in #36159."
  },
  {
   "t": "2026-09-03T10:45:48Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "c43d544b16b61e3182467dba7355cdaa372204c6",
   "text": "Decided to split out two PRs from this one as they are quite independent. See #36159 and #36160."
  },
  {
   "t": "2026-09-03T13:19:19Z",
   "kind": "review",
   "who": "janb84",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "c43d544b16b61e3182467dba7355cdaa372204c6",
   "text": "(re) ACK c43d544b16b61e3182467dba7355cdaa372204c6\n\nreturning the state and not a bool at the `load()` function, is the better way to go imho :) Have re-reviewed the PR because of the split-off; build tested etc.\n\nIn the commit message, is an API change a behaviour change? super micro nit.\nThe `load()` documentation block has no @return anymore, is state super clear to everyone ?"
  },
  {
   "t": "2026-09-05T20:21:56Z",
   "kind": "comment",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nhad not noticed this. i think we can add the `@return` with some _light_\ndocumentation.\n\n`State` is self-explanatory as a type, but it does not tell a caller that\n`Load()` never returns `Error` (it throws instead), or that the other values\nmean \"_keep calling with more data_\" vs \"_done_\", sth that is specific to\n`Load()`'s contract. secondly, it keeps consistency with the `@throws` below\nand the Doxygen style used elsewhere"
  },
  {
   "t": "2026-09-05T20:52:20Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "62796e659fb79a0c6f1749677ec480a80ec26e6d"
  },
  {
   "t": "2026-09-05T21:00:03Z",
   "kind": "comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "text": "Latest push modifies the initial `Load()` commit only.\n\nre https://github.com/bitcoin/bitcoin/pull/36124#pullrequestreview-5102409506:\n[quoted text omitted]\n\nGood point, fixed.\n\nre https://github.com/bitcoin/bitcoin/pull/36124#pullrequestreview-5102409506 + https://github.com/bitcoin/bitcoin/pull/36124#issuecomment-5554537521:\n[quoted text omitted]\n\nAdded back `@returns`. It is true that `Load()` (re-)throws the first time it sets `Error`, but subsequent calls will return `Error`. The return value is only read in unit tests, so not sure if it's worth elaborating further in the doc-string."
  },
  {
   "t": "2026-09-05T21:24:33Z",
   "kind": "comment",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "text": "tACK 62796e659fb79a0c6f1749677ec480a80ec26e6d\n\nsuccessfully built and tested on this toolchain: Debian/`clang++-18`/`x86_64`"
  },
  {
   "t": "2026-09-10T18:14:23Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "92045f9bf6585690708b16a33b67792f50c74016"
  },
  {
   "t": "2026-09-16T16:28:57Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "08d0347acb812ced579f782e4a2daf2c4d81546e"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-31T07:58:57Z",
   "action": "labeled",
   "label": "RPC/REST/ZMQ",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-03T09:05:44Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-03T10:35:39Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-10T16:07:44Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-10T18:37:44Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-16T08:55:54Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-16T17:51:49Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-05T20:51:47Z",
   "kind": "renamed",
   "who": "hodlinator",
   "from": "http: Make `HTTPRequest` update state internally, etc",
   "to": "http: Make `HTTPRequest` update state internally"
  }
 ],
 "text_chars": 8282,
 "text_tokens_estimate": 2070,
 "changed_paths": [
  "src/httpserver.cpp",
  "src/httpserver.h",
  "src/test/fuzz/http_request.cpp",
  "src/test/httpserver_tests.cpp"
 ],
 "files": [
  {
   "path": "src/httpserver.cpp",
   "add": 45,
   "del": 41
  },
  {
   "path": "src/httpserver.h",
   "add": 26,
   "del": 25
  },
  {
   "path": "src/test/fuzz/http_request.cpp",
   "add": 2,
   "del": 4
  },
  {
   "path": "src/test/httpserver_tests.cpp",
   "add": 44,
   "del": 74
  }
 ],
 "test_lines": 124,
 "git": {
  "head": "08d0347acb812ced579f782e4a2daf2c4d81546e",
  "head_matches_backup": true,
  "base": "b8215855437288cac95c7fd5d41104c73c0f3495",
  "commits": [
   {
    "sha": "6983eb0d49",
    "subject": "http: Extract HTTPRequest::Load() to remove SetState()",
    "files": 4,
    "add": 96,
    "del": 114
   },
   {
    "sha": "ef877990ea",
    "subject": "refactor: Extract HTTPRemoteClient::TryReadRequestInternal() from static HTTPRemoteClient::TryReadRequest()",
    "files": 2,
    "add": 25,
    "del": 18
   },
   {
    "sha": "08d0347acb",
    "subject": "refactor: Flatten HTTPRemoteClient::ReadRequest() into HTTPRemoteClient::TryReadRequestInternal()",
    "files": 2,
    "add": 13,
    "del": 29
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "9498c899f4fd21a3",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}