{
 "number": 36198,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36198",
 "title": "http: Add missing LIFETIMEBOUND annotations",
 "author": "hodlinator",
 "author_association": "MEMBER",
 "created_at": "2026-09-09T08:06:00Z",
 "updated_at": "2026-09-09T08:06:15Z",
 "age_days": 8,
 "draft": false,
 "labels": [
  "RPC/REST/ZMQ"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "0b46fc9c5935068e7068a27fa39fa11f32af7a3f",
 "head_ref": "2026/09/http_dangling",
 "head_repo": "hodlinator/bitcoin",
 "head_history": [],
 "additions": 6,
 "deletions": 6,
 "changed_files": 1,
 "commit_count": 1,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": []
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-08T08:11:24Z",
  "last_reviewer_activity": null,
  "last_reviewer": null,
  "author_silent_days": 9,
  "waiting_on_author_days": 0,
  "days_since_update": 8
 },
 "refs": {
  "mentioned": [
   36164
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 36164,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-06",
    "title": "util: diagnose dangling views of temporary strings"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "Helps Clang detect certain dangling reference issues, in a similar vein as #36164.\n\n### Known limitations\n\nIt doesn't catch invalidation nor brace-initialization.\n\nDiff illustrating limitations\n\n```diff\n--- a/src/test/httpserver_tests.cpp\n+++ b/src/test/httpserver_tests.cpp\n@@ -80,6 +80,15 @@ BOOST_AUTO_TEST_CASE(test_query_parameters)\n\n BOOST_AUTO_TEST_CASE(http_headers_tests)\n {\n+    auto foo = HTTPHeaders{}.FindAll(\"needle\"); // Emits warning\n+    (void)foo;\n+    auto bar{HTTPHeaders{}.FindAll(\"needle\")}; // No warning with Clang 22.1.8 :/\n+    (void)bar;\n+\n+    HTTPHeaders test;\n+    auto baz = test.FindAll(\"needle\");\n+    test.Write(\"needle\", \"mutation\"); // No warning with Clang 22.1.8 :/\n+\n     {\n         // Writing response headers\n         HTTPHeaders headers{};\n```\n\nClang 24 has experimental invalidation detection so maybe that could be used in the far future: https://clang.llvm.org/docs/LifetimeSafety.html#use-after-invalidation-experimental\n\n### Alternative solution A)\n\nReturn by copy everywhere. Might introduce more heap activity, especially in the case of `HTTPRemoteClient::GetRequest()`.\n\n### Alternative solution B)\n\nRefactor the methods to minimize copying while still making things more memory-safe. Replacing `HTTPHeaders::FindAll()` with an `Iterate()`-function taking a lambda which gets to process each header. Gets rid of the heap activity of building a `vector` but introduces copying of `first`.\n\nDiff of httpserver.cpp/h\n\n```diff\n--- a/src/httpserver.cpp\n+++ b/src/httpserver.cpp\n@@ -272,15 +272,11 @@ std::optional<std::string> HTTPHeaders::FindFirst(const std::string_view key) co\n     return std::nullopt;\n }\n\n-std::vector<std::string_view> HTTPHeaders::FindAll(const std::string_view key) const\n+void HTTPHeaders::Iterate(std::function<void(const std::string& key, const std::string& value)> fn) const\n {\n-    std::vector<std::string_view> ret;\n     for (const auto& item : m_headers) {\n-        if (CaseInsensitiveEqual(key, item.first)) {\n-            ret.push_back(item.second);\n-        }\n+        fn(item.first, item.second);\n     }\n-    return ret;\n }\n\n void HTTPHeaders::Write(std::string&& key, std::string&& value)\n@@ -504,18 +500,21 @@ bool HTTPRequest::LoadBody(LineReader& reader)\n         // We read all the chunks but never got the last chunk, wait for client to send more\n         return false;\n     } else {\n+        std::optional<std::string> first;\n+        m_headers.Iterate([&first] (const std::string& key, const std::string& value) {\n+            if (!CaseInsensitiveEqual(key, \"Content-Length\")) return;\n+            if (!first.has_value()) {\n+                first = value;\n+            } else if (first != value) {\n+                // Duplicate Content-Length headers are allowed only if they all have the same value\n+                // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.3\n+                throw std::runtime_error(\"Differing Content-Length values\");\n+            }\n+        });\n         // No Content-length or Transfer-Encoding header means no body, see libevent evhttp_get_body()\n-        auto content_length_values{m_headers.FindAll(\"Content-Length\")};\n-        if (content_length_values.empty()) return true;\n-\n-        // Duplicate Content-Length headers are allowed only if they all have the same value\n-        // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.3\n-        const auto& first_content_length_value{content_length_values[0]};\n-        for (size_t i = 1; i < content_length_values.size(); ++i) {\n-            if (content_length_values[i] != first_content_length_value) throw std::runtime_error(\"Differing Content-Length values\");\n-        }\n+        if (!first.has_value()) return true;\n\n-        const auto content_length{ToIntegral<uint64_t>(first_content_length_value)};\n+        const auto content_length{ToIntegral<uint64_t>(first.value())};\n         if (!content_length) throw std::runtime_error(\"Cannot parse Content-Length value\");\n\n         if (*content_length > MAX_BODY_SIZE) throw ContentTooLargeError(\"Max body size exceeded\");\n--- a/src/httpserver.h\n+++ b/src/httpserver.h\n@@ -97,10 +97,9 @@ public:\n      */\n     std::optional<std::string> FindFirst(std::string_view key) const;\n     /**\n-     * @param[in] key The field-name of the header to search for\n-     * @returns Views into all values matching the provided key (valid while this object is alive)\n+     * @param[in] fn Receives each header as they are iterated through.\n      */\n-    std::vector<std::string_view> FindAll(std::string_view key) const LIFETIMEBOUND;\n+    void Iterate(std::function<void(const std::string& key, const std::string& value)> fn) const;\n     void Write(std::string&& key, std::string&& value);\n     /**\n      * @param[in] key The field-name of the header to search for and delete\n```\n\n### Rationale\n\nThe methods are not called in many places so risk of misuse is low, and we avoid any risk of performance degradation (such as the one found in https://github.com/bitcoin/bitcoin/pull/35182#discussion_r3266022600). Return copies without adding mutexes or other thread safety measures does not considerably increase thread-safety.",
 "commits": [
  {
   "sha": "0b46fc9c5935068e7068a27fa39fa11f32af7a3f",
   "date": "2026-09-08T08:11:24Z",
   "message": "http: Add missing LIFETIMEBOUND annotations\n\nDecreases footguns without any hit to runtime performance."
  }
 ],
 "timeline": [],
 "labels_log": [
  {
   "t": "2026-09-09T08:06:04Z",
   "action": "labeled",
   "label": "RPC/REST/ZMQ",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 5286,
 "text_tokens_estimate": 1321,
 "changed_paths": [
  "src/httpserver.h"
 ],
 "files": [
  {
   "path": "src/httpserver.h",
   "add": 6,
   "del": 6
  }
 ],
 "test_lines": 0,
 "git": {
  "head": "0b46fc9c5935068e7068a27fa39fa11f32af7a3f",
  "head_matches_backup": true,
  "base": "013b0b2de48153b7481c036a353b81a411f1da8f",
  "commits": [
   {
    "sha": "0b46fc9c59",
    "subject": "http: Add missing LIFETIMEBOUND annotations",
    "files": 1,
    "add": 6,
    "del": 6
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "f3862c53fe0d3bfc",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}