{
 "number": 36159,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36159",
 "title": "http: Improve HTTPRemoteClient::MaybeDisconnect()",
 "author": "hodlinator",
 "author_association": "MEMBER",
 "created_at": "2026-09-03T10:41:02Z",
 "updated_at": "2026-09-17T06:07:22Z",
 "age_days": 14,
 "draft": false,
 "labels": [
  "RPC/REST/ZMQ"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "352fdc279751694348c6b406322de5582f59ad49",
 "head_ref": "2026/09/should_disconnect",
 "head_repo": "hodlinator/bitcoin",
 "head_history": [
  {
   "t": "2026-09-05T20:32:16Z",
   "sha": "352fdc279751694348c6b406322de5582f59ad49"
  }
 ],
 "additions": 152,
 "deletions": 44,
 "changed_files": 3,
 "commit_count": 5,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "winterrdog",
      "url": "https://github.com/bitcoin/bitcoin/pull/36159#issuecomment-5555746113"
     }
    ],
    "concept_ack": [
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/36159#pullrequestreview-5114081037"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36204,
     "title": "http: disconnect clients that never finish a request",
     "author": "janb84"
    },
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    }
   ]
  }
 },
 "acks_parsed": {
  "jeanpablojp": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-04T14:19:11Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "jeanpablojp",
   "winterrdog"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-05T20:39:43Z",
  "last_reviewer_activity": "2026-09-06T00:19:24Z",
  "last_reviewer": "winterrdog",
  "author_silent_days": 11,
  "waiting_on_author_days": 11,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   35182,
   35829
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35182,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-06-22",
    "title": "Replace libevent with our own HTTP and socket-handling implementation"
   },
   {
    "number": 35829,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-08-26",
    "title": "http: Make class fields private and make HTTPResponse a struct"
   }
  ],
  "conflicts": [
   36204,
   35713
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/httpserver.cpp",
  "src/test/httpserver_tests.cpp"
 ],
 "body": "### Issues\n\n#35182 had 1 case where we flag a client for disconnect where we do not log the reason.\n\n#35829 introduced the somewhat poorly named non-`const` `HTTPRemoteClient::MaybeDisconnect()`. It also lacks testing.\n\nThe control-flow in `MaybeDisconnect()/ShouldDisconnect()` is somewhat complex. https://github.com/bitcoin/bitcoin/pull/35829#discussion_r3846342291\n\n### Commits solving these issues in same order\n\n* http: Add log message for remaining case where we set `HTTPRemoteClient::m_disconnect = true`\n* refactor: Rename `MaybeDisconnect()` to `ShouldDisconnect()` and make it `const`\n* refactor: Enable mocking the time around `ShouldDisconnect()`\n* test: Add test for behavior of `ShouldDisconnect()`\n* refactor: Flatten control flow in `ShouldDisconnect()`",
 "commits": [
  {
   "sha": "a7ffe7acba3ef0907ef53da805d9a26c93ee7232",
   "date": "2026-09-03T10:16:10Z",
   "message": "http: Add log message for remaining case where we set m_disconnect = true"
  },
  {
   "sha": "b0e7b0cc52ab0d1e2735f6645b7c00e1fa1effec",
   "date": "2026-09-03T10:16:10Z",
   "message": "refactor: Rename MaybeDisconnect() to ShouldDisconnect() and make it const\n\nAlso reins in whitespace while touching the lambda (\"Maybe\" is one char shorter than \"Should\" so aligning whitespace would happen anyway)."
  },
  {
   "sha": "9afe15897e8dbee9429722d0db7859d895919bf4",
   "date": "2026-09-05T20:31:24Z",
   "message": "refactor: Switch client idle timeout to mockable clock\n\nEnables precision in later tests."
  },
  {
   "sha": "784ac4fb9b4b3b9a5614e4ea54c108e1246ce50a",
   "date": "2026-09-05T20:31:24Z",
   "message": "test: Verify behavior of HTTPRemoteClient::ShouldDisconnect()\n\nBest to nail this down before the refactor in the next commit.\n\nCo-authored-by: JP <jeanpablo.jp@hotmail.com>"
  },
  {
   "sha": "352fdc279751694348c6b406322de5582f59ad49",
   "date": "2026-09-05T20:31:24Z",
   "message": "http: Flatten control flow in HTTPRemoteClient::ShouldDisconnect()\n\nThere's a slight behavior change in that clients already flagged with m_disconnect = true no longer also get logged as passing the idle timeout if that condition would also be met.\n\nCo-authored-by: winterrdog <winterrdog@protonmail.ch>"
  }
 ],
 "timeline": [
  {
   "t": "2026-09-04T14:19:11Z",
   "kind": "review",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "332f4e48c51128f4164c56b4bfbe908b83f5b08f",
   "text": "Concept ACK\n\nBuilt the four commits and mutation-tested the changed branches.\n\n`now` is a whole second, taken before the clients exist and compared against an `m_idle_since` they stamp later, so the checks expecting a disconnect at `now + timeout + 1s` only hold while both fall in the same second. About 0.6ms sits in between on `busy_slow`, and a rollover there makes the comparison `30s > 30s`. Forced one to confirm. Worth taking `now` per block, after `WriteReply()` for `busy_slow`?"
  },
  {
   "t": "2026-09-04T14:19:11Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/httpserver_tests.cpp",
   "commit": "332f4e48c51128f4164c56b4bfbe908b83f5b08f",
   "in_reply_to": null,
   "text": "`ZeroSock::Send()` already returns `len`, so could this just be a `ZeroSock`?\n\n```suggestion\n        // Allow disconnecting directly after WriteReply() when it is able to\n        // complete the optimistic send of the full response due to the ZeroSock.\n        auto busy_fast{std::make_shared<Client>(std::make_unique<ZeroSock>())};\n```"
  },
  {
   "t": "2026-09-04T14:19:11Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/httpserver.cpp",
   "commit": "352fdc279751694348c6b406322de5582f59ad49",
   "in_reply_to": null,
   "text": "This also stops the idle line being logged when the client is already flagged. Before, `is_idle` was worked out first and logged inside `if (m_disconnect || is_idle)`, so a client already flagged and past the timeout got both lines. Looks like an improvement, since that line was claiming a timeout for a client leaving for another reason. Worth a mention in the commit message, since it says `refactor:`?"
  },
  {
   "t": "2026-09-04T14:19:12Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/httpserver_tests.cpp",
   "commit": "352fdc279751694348c6b406322de5582f59ad49",
   "in_reply_to": null,
   "text": "A connection that is accepted and never sends anything keeps `m_connection_busy` at its initial `true`, so the `disconnect_all` branch never picks it up. Inside `ShouldDisconnect()` only the idle timeout is left, and with `-rpcservertimeout=0` there is no idle timeout either. On regtest shutdown goes from 0.7s to 30.7s and ends in a `Force-disconnecting` warning. At the default of 30s that timeout and `StopHTTPServer()`'s own deadline are the same number. Releasing them at once would give up the `503` a client can still get today by sending after `stop`. Is the wait deliberate?"
  },
  {
   "t": "2026-09-04T14:19:12Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/httpserver_tests.cpp",
   "commit": "352fdc279751694348c6b406322de5582f59ad49",
   "in_reply_to": null,
   "text": "Every client here is `HTTP/1.0`, so `m_keep_alive` is never true and the branch for a healthy persistent connection is only ever asserted not firing. Replacing that condition with `false` leaves this test green. Worth one more client?\n\n```suggestion\n    {\n        // A keep-alive client whose reply is out is neither flagged nor idle,\n        // so the shutdown branch is the only thing that can release it.\n        auto keepalive{std::make_shared<Client>(std::make_unique<ZeroSock>())};\n        keepalive->MutateRecvBuffer().append(\"GET / HTTP/1.1\\nHost: 127.0.0.1\\n\\n\");\n        {\n            std::unique_ptr request{HTTPRemoteClient::TryReadRequest(keepalive)};\n            BOOST_REQUIRE(request);\n            request->WriteReply(HTTP_OK);\n        }\n        const auto now{Now<SteadySeconds>()};\n        BOOST_CHECK(!keepalive->ShouldDisconnect(now, timeout, /*disconnect_all=*/false));\n        BOOST_CHECK( keepalive->ShouldDisconnect(now, timeout, /*disconnect_all=*/true));\n    }\n\n    {\n        // A malformed request flags the client, and that outranks the rest.\n```"
  },
  {
   "t": "2026-09-05T19:34:28Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/httpserver_tests.cpp",
   "commit": "352fdc279751694348c6b406322de5582f59ad49",
   "in_reply_to": 3934900193,
   "text": "re https://github.com/bitcoin/bitcoin/pull/36159#discussion_r3934900182:\nYou are pointing out behavior on master, right?\nMaybe we could disconnect clients quicker if they just open the connection and don't send anything, within 5 seconds or so? Not sure I want to increase the complexity of this PR though. That would probably introduce another timeout-setting."
  },
  {
   "t": "2026-09-05T20:32:16Z",
   "kind": "force_push",
   "who": "hodlinator",
   "commit": "352fdc279751694348c6b406322de5582f59ad49"
  },
  {
   "t": "2026-09-05T20:39:43Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "352fdc279751694348c6b406322de5582f59ad49",
   "text": "Thanks for the feedback @jeanpablojp (https://github.com/bitcoin/bitcoin/pull/36159#pullrequestreview-5114081037)!\n\nAddressed all of it to some extent in the latest push (352fdc279751694348c6b406322de5582f59ad49) + inline comment. Opted for mocking the clock and freeze it rather than trying to keep up with the current time of the actual clock as it drifts throughout the added test case."
  },
  {
   "t": "2026-09-05T22:27:13Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/httpserver_tests.cpp",
   "commit": "352fdc279751694348c6b406322de5582f59ad49",
   "in_reply_to": 3934900193,
   "text": "Yes, master, but also measured at the merge-base and on today's master, the same both times, under a second to shut down with no such connection open and 30.7s with one at `-rpcservertimeout=0`.\n\nThe slots are the sharper half. With `-rpcmaxconnections=4` and four sockets that connect and send nothing, a normal call has to wait for the idle timeout to free one, and `-rpcservertimeout=0` means nothing frees them. Agreed it does not belong here though, and your five seconds sounds like the right direction. Happy to open a follow-up."
  },
  {
   "t": "2026-09-06T00:19:24Z",
   "kind": "comment",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "text": "tACK 352fdc279751694348c6b406322de5582f59ad49\n\nsuccessfully built and tested on this toolchain: Debian/`clang++ 18`/`x86_64`.\n\n---\nP.S: the CI failure is completely independent of the changes introduced by this PR"
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-03T10:41:06Z",
   "action": "labeled",
   "label": "RPC/REST/ZMQ",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-05T21:31:23Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-17T06:07:22Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 6071,
 "text_tokens_estimate": 1517,
 "changed_paths": [
  "src/httpserver.cpp",
  "src/httpserver.h",
  "src/test/httpserver_tests.cpp"
 ],
 "files": [
  {
   "path": "src/httpserver.cpp",
   "add": 37,
   "del": 41
  },
  {
   "path": "src/httpserver.h",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/test/httpserver_tests.cpp",
   "add": 112,
   "del": 0
  }
 ],
 "test_lines": 112,
 "git": {
  "head": "352fdc279751694348c6b406322de5582f59ad49",
  "head_matches_backup": true,
  "base": "d840adb9da54cc304b8ca95761964ab92d1ac51a",
  "commits": [
   {
    "sha": "a7ffe7acba",
    "subject": "http: Add log message for remaining case where we set m_disconnect = true",
    "files": 2,
    "add": 8,
    "del": 0
   },
   {
    "sha": "b0e7b0cc52",
    "subject": "refactor: Rename MaybeDisconnect() to ShouldDisconnect() and make it const",
    "files": 2,
    "add": 7,
    "del": 8
   },
   {
    "sha": "9afe15897e",
    "subject": "refactor: Switch client idle timeout to mockable clock",
    "files": 2,
    "add": 7,
    "del": 7
   },
   {
    "sha": "784ac4fb9b",
    "subject": "test: Verify behavior of HTTPRemoteClient::ShouldDisconnect()",
    "files": 1,
    "add": 107,
    "del": 0
   },
   {
    "sha": "352fdc2797",
    "subject": "http: Flatten control flow in HTTPRemoteClient::ShouldDisconnect()",
    "files": 2,
    "add": 27,
    "del": 33
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "6eb5846bfa35e0ef",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}