{
 "number": 35780,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35780",
 "title": "http: linger-close after parse errors so clients can read the reply",
 "author": "b-l-u-e",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-07-22T21:52:38Z",
 "updated_at": "2026-08-27T14:38:02Z",
 "age_days": 56,
 "draft": true,
 "labels": [
  "RPC/REST/ZMQ",
  "Needs rebase"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "b14649f9fead1bbb75e035148672f376912b1835",
 "head_ref": "http-lingering-close",
 "head_repo": "b-l-u-e/bitcoin",
 "head_history": [
  {
   "t": "2026-07-28T19:44:46Z",
   "sha": "1b34df59378f1349107ac9bbbdb0f455b36e8838"
  },
  {
   "t": "2026-07-28T19:56:35Z",
   "sha": "06ae35c3417b7adb0c6f258ab5ea7d08ec53205c"
  },
  {
   "t": "2026-07-28T20:00:39Z",
   "sha": "2f14e212e853856e7d9eb7dc18bce3aa2fe64f95"
  },
  {
   "t": "2026-08-24T09:39:24Z",
   "sha": "1e0e718c7a919fd5caef4be4556ab96c7236b2c2"
  },
  {
   "t": "2026-08-24T10:26:13Z",
   "sha": "b14649f9fead1bbb75e035148672f376912b1835"
  }
 ],
 "additions": 139,
 "deletions": 21,
 "changed_files": 8,
 "commit_count": 2,
 "size_bucket": "M",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "pinheadmz",
      "url": "https://github.com/bitcoin/bitcoin/pull/35780#pullrequestreview-4811176585"
     },
     {
      "login": "frankomosh",
      "url": "https://github.com/bitcoin/bitcoin/pull/35780#pullrequestreview-4920304038"
     },
     {
      "login": "winterrdog",
      "url": "https://github.com/bitcoin/bitcoin/pull/35780#pullrequestreview-4966116116"
     },
     {
      "login": "hodlinator",
      "url": "https://github.com/bitcoin/bitcoin/pull/35780#pullrequestreview-5041951268"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35829,
     "title": "http: Make class fields private and make HTTPResponse a struct",
     "author": "hodlinator"
    }
   ]
  }
 },
 "acks_parsed": {
  "pinheadmz": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-07-29T17:38:37Z",
   "stale": false
  },
  "frankomosh": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-12T19:18:10Z",
   "stale": false
  },
  "winterrdog": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-18T21:55:44Z",
   "stale": false
  },
  "hodlinator": {
   "kind": "concept_ack",
   "hash": "b14649f9fead1bbb75e035148672f376912b1835",
   "t": "2026-08-27T14:37:58Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 4,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "151henry151",
   "fanquake",
   "frankomosh",
   "hodlinator",
   "pinheadmz",
   "winterrdog"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-08-24T10:26:13Z",
  "last_reviewer_activity": "2026-08-27T14:37:58Z",
  "last_reviewer": "hodlinator",
  "author_silent_days": 24,
  "waiting_on_author_days": 21,
  "days_since_update": 21
 },
 "refs": {
  "mentioned": [
   35632,
   35735
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35632,
    "type": "issue",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "intermittent ci failure: interface_http.py\\\", line 558, in check_whitespace_in_headers // ConnectionAbortedError: [WinError 10053] An established connection was aborted by the software in your host machine"
   },
   {
    "number": 35735,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-08-17",
    "title": "Add state to HTTPRequest"
   }
  ],
  "conflicts": [
   35829
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/httpserver.cpp"
 ],
 "body": "Based on this issue #35632 raised i believe the issue is not with whitespace header validation since the logic works fine coz the server detects the malformed request and queues a 400 reply as expected.\n\nThe problem is how the server closes the connection afterwards. It queues the reply and closes the socket right away sometimes before it has finished reading everything the client had sent.\nSo when socket is closed while there's still unread data sitting in the kernel's receive buffer, Windows responds with a TCP RST. That reset just throws away the reply that was already on its way out, so the client never gets to read the 400 and instead sees the connection aborted.\n\nSo my LLM Buddie and I realized that we should change how the server closes a connection after a parse/size error so the client always gets a chance to read the response first by queuing the `400/413 response` with `Connection: close`.\nThen, waits until the response has actually left the send buffer. Then, Half-close only the write side (shutdown(SHUT_WR/SD_SEND)) so the client can still finish reading while the server stops sending. Afterwards, Keep draining whatever the client is still sending until it hits EOF or a socket error and only force the connection closed after a 1s fallback timeout, for a client that never finishes.",
 "commits": [
  {
   "sha": "c31ea96456a0e8f65938a1deb91202686ed84517",
   "date": "2026-08-24T08:43:33Z",
   "message": "util: add Sock::ShutdownSend() to half-close the send side\n\nWrap shutdown(2) (SHUT_WR, or SD_SEND on Windows) behind the Sock interface so it can be overridden by the mock sockets used in the unit and fuzz tests."
  },
  {
   "sha": "b14649f9fead1bbb75e035148672f376912b1835",
   "date": "2026-08-24T10:25:48Z",
   "message": "http: linger-close after parse errors so clients can read the reply\n\nWhen the HTTP server rejects a request during parsing a malformed header or an oversized body, it replied and then immediately closed the connection. On Windows, closing a socket that still has unread inbound data can trigger a TCP RST, which discards the queued reply before the client reads it, so the client sees a connection reset instead of the 400/413 response (see #35632).\nInstead of closing abruptly, flush the error reply, half-close the send side, and drain any remaining inbound data until the peer sends EOF. A 1s LINGERING_CLOSE_TIMEOUT bounds clients that never close their side."
  }
 ],
 "timeline": [
  {
   "t": "2026-07-28T19:44:46Z",
   "kind": "force_push",
   "who": "b-l-u-e",
   "commit": "1b34df59378f1349107ac9bbbdb0f455b36e8838"
  },
  {
   "t": "2026-07-28T19:56:35Z",
   "kind": "force_push",
   "who": "b-l-u-e",
   "commit": "06ae35c3417b7adb0c6f258ab5ea7d08ec53205c"
  },
  {
   "t": "2026-07-28T20:00:39Z",
   "kind": "force_push",
   "who": "b-l-u-e",
   "commit": "2f14e212e853856e7d9eb7dc18bce3aa2fe64f95"
  },
  {
   "t": "2026-07-29T17:26:40Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "Is there any way to cover the new behavior with functional tests? For example, maybe in `interface_http` can assertions be added to the try/except blocks? Like, does this PR eliminate those race conditions?"
  },
  {
   "t": "2026-07-29T17:29:49Z",
   "kind": "review_comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": null,
   "text": "I'm not sure why we need a flag `force_close` in addition to `HTTPRemoteClient.m_disconnect`?"
  },
  {
   "t": "2026-07-29T17:31:15Z",
   "kind": "review_comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": null,
   "text": "If we're just draining the socket and ignoring the data anyway, do we still need to copy it into memory at all? (And then clear it immediately?)"
  },
  {
   "t": "2026-07-29T17:33:04Z",
   "kind": "review_comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": null,
   "text": "I think if we need this level of accuracy here, we can just replace the seconds counter with ms and only use one. See https://github.com/bitcoin/bitcoin/pull/35182#discussion_r3474631037"
  },
  {
   "t": "2026-07-29T17:34:51Z",
   "kind": "review_comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": null,
   "text": "I don't totally understand why we need a specific function for this, like a linger close is a special type of close? Why don't we just make it so every client close (for any reason) drains the incoming socket for up to 1 second first?"
  },
  {
   "t": "2026-07-29T17:36:45Z",
   "kind": "review_comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": null,
   "text": "I don't think we need to guard this, since a lingering close happens in the middle of a request being processed already. This will be even better handled after #35735 when we won't even read from the socket if one request is being handled."
  },
  {
   "t": "2026-07-29T17:38:37Z",
   "kind": "review",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "2f14e212e853856e7d9eb7dc18bce3aa2fe64f95",
   "text": "Concept ACK on lingering close to be more polite to clients and improve test determinism. However I am not convinced about the approach. I also think this is a feature that would be easier to implement after #35735"
  },
  {
   "t": "2026-07-30T17:05:44Z",
   "kind": "review_comment",
   "who": "b-l-u-e",
   "assoc": "CONTRIBUTOR",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": 3676651684,
   "text": "the force_close is about HTTP response, send connection:close and dont keep-alive\nthen for m_disconnect is for tear down this socket now .. so we need force_close first then m_disconnect later in order to tell client we are done flush the error reply  drain a little bit then close so if we set m_disconnect immediately will close too early that will hit RST and hide the reply"
  },
  {
   "t": "2026-07-30T17:07:08Z",
   "kind": "review_comment",
   "who": "b-l-u-e",
   "assoc": "CONTRIBUTOR",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": 3676662160,
   "text": "i agree  i will clean it up"
  },
  {
   "t": "2026-07-30T17:07:25Z",
   "kind": "review_comment",
   "who": "b-l-u-e",
   "assoc": "CONTRIBUTOR",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": 3676674505,
   "text": "agreed will update"
  },
  {
   "t": "2026-07-30T17:07:50Z",
   "kind": "review_comment",
   "who": "b-l-u-e",
   "assoc": "CONTRIBUTOR",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": 3676687831,
   "text": "well linger isnt needed for every close its useful when we reject a request while client is still sending huge body or bad request so if we close with unread data still sitting there then windows can RST and client never sees 400 or 413"
  },
  {
   "t": "2026-07-30T17:09:18Z",
   "kind": "comment",
   "who": "b-l-u-e",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\ni will wait for your PR to be merged first"
  },
  {
   "t": "2026-07-30T17:09:27Z",
   "kind": "review_comment",
   "who": "b-l-u-e",
   "assoc": "CONTRIBUTOR",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": 3676701718,
   "text": "i thinkk  we still want the guard because once we are lingering that connection should only drain and go away not keep parsing or dispatching more work\nin #35735 helps with one request at time less queue weirdnes but doesnt remove the need for if we are shutting this connection via linger doesnt treat it as a normal live client"
  },
  {
   "t": "2026-08-10T09:13:50Z",
   "kind": "comment",
   "who": "fanquake",
   "assoc": "MEMBER",
   "text": "I've moved this to draft for now. In the mean time, @b-l-u-e if you want to rebase this on top of #35735, that could be worthwhile."
  },
  {
   "t": "2026-08-11T17:39:55Z",
   "kind": "review_comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": null,
   "text": "I think the motivation behind this PR is to be able to honestly remove this comment? Even in case of an error, we shouldn't drop the response for the client."
  },
  {
   "t": "2026-08-12T19:18:10Z",
   "kind": "review",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "2f14e212e853856e7d9eb7dc18bce3aa2fe64f95",
   "text": "Concept ACK. Agree the goal here is worth solving"
  },
  {
   "t": "2026-08-13T01:27:00Z",
   "kind": "comment",
   "who": "151henry151",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nThe try/excepts in check_excessive_request_size and check_chunked_transfer are the 413 cases, where the client is still uploading when we reject. Drain-until-EOF is meant for that, and on loopback it probably does make those reliable. I still wouldn't turn them into hard asserts \u2014 if the upload hasn't finished by the fallback timeout, that's a flake that only shows up on slow runners.\n\nThe #35632 flake is the other way around: we reject at the headers and close with the body still unread, so Windows RST's and the 400 never arrives. check_whitespace_in_headers already asserts 400."
  },
  {
   "t": "2026-08-18T21:55:44Z",
   "kind": "review",
   "who": "winterrdog",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "2f14e212e853856e7d9eb7dc18bce3aa2fe64f95",
   "text": "concept ACK\n\nfrom https://github.com/bitcoin/bitcoin/pull/35780#pullrequestreview-4811176585 :\n[quoted text omitted]\n\nfully agree with this comment\n\n---\n### my rough idea of how i think it can be approached:\n_if rebased on #35735_: once `m_req->m_state` transitions to an `HTTPRequest::State::Error` state, we know HTTP processing for that connection is *all over*. so, queue the 400/413, stop buffering or parsing further input, let the existing send path flush out the response, then half-close the send side and simply discard incoming data from the kernel buffer until EOF or an error or a timeout, giving well-behaved clients room to disconnect. in other words, the request state machine handles the HTTP side, and only minimal connection-level state is needed for the final TCP drain (kernel-side)"
  },
  {
   "t": "2026-08-24T09:39:24Z",
   "kind": "force_push",
   "who": "b-l-u-e",
   "commit": "1e0e718c7a919fd5caef4be4556ab96c7236b2c2"
  },
  {
   "t": "2026-08-24T10:26:13Z",
   "kind": "force_push",
   "who": "b-l-u-e",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835"
  },
  {
   "t": "2026-08-27T14:26:33Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "CONTRIBUTOR",
   "path": "src/httpserver.cpp",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "in_reply_to": null,
   "text": "Why not write this, seems fine to have the other side close on us?"
  },
  {
   "t": "2026-08-27T14:37:58Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "b14649f9fead1bbb75e035148672f376912b1835",
   "text": "Concept ACK b14649f9fead1bbb75e035148672f376912b1835\n\nThanks for working on this! Since I'm to blame for the merge conflict, here's a suggestion for how to rebase: https://github.com/bitcoin/bitcoin/compare/master...hodlinator:bitcoin:pr/35780_rebased"
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-22T21:52:41Z",
   "action": "labeled",
   "label": "RPC/REST/ZMQ",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-28T19:57:13Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-28T21:02:57Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-10T10:11:36Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-24T09:58:33Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-24T11:30:15Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-24T11:49:41Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-26T11:19:06Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-07-23T15:43:18Z",
   "kind": "convert_to_draft",
   "who": "b-l-u-e"
  },
  {
   "t": "2026-07-23T19:05:55Z",
   "kind": "renamed",
   "who": "b-l-u-e",
   "from": "http: linger-close after parse errors so clients can read the reply",
   "to": "[WIP] http: linger-close after parse errors so clients can read the reply"
  },
  {
   "t": "2026-07-28T19:23:08Z",
   "kind": "renamed",
   "who": "b-l-u-e",
   "from": "[WIP] http: linger-close after parse errors so clients can read the reply",
   "to": "http: linger-close after parse errors so clients can read the reply"
  },
  {
   "t": "2026-07-29T14:57:15Z",
   "kind": "ready_for_review",
   "who": "b-l-u-e"
  },
  {
   "t": "2026-08-10T09:09:53Z",
   "kind": "convert_to_draft",
   "who": "fanquake"
  }
 ],
 "text_chars": 6717,
 "text_tokens_estimate": 1679,
 "changed_paths": [
  "src/httpserver.cpp",
  "src/httpserver.h",
  "src/test/fuzz/util/net.cpp",
  "src/test/fuzz/util/net.h",
  "src/test/util/net.cpp",
  "src/test/util/net.h",
  "src/util/sock.cpp",
  "src/util/sock.h"
 ],
 "files": [
  {
   "path": "src/httpserver.cpp",
   "add": 84,
   "del": 17
  },
  {
   "path": "src/httpserver.h",
   "add": 32,
   "del": 4
  },
  {
   "path": "src/test/fuzz/util/net.cpp",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/test/fuzz/util/net.h",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/test/util/net.cpp",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/test/util/net.h",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/util/sock.cpp",
   "add": 9,
   "del": 0
  },
  {
   "path": "src/util/sock.h",
   "add": 6,
   "del": 0
  }
 ],
 "test_lines": 8,
 "git": {
  "head": "b14649f9fead1bbb75e035148672f376912b1835",
  "head_matches_backup": true,
  "base": "58a7869f860f022e6163dc3fae3106a0599830c5",
  "commits": [
   {
    "sha": "c31ea96456",
    "subject": "util: add Sock::ShutdownSend() to half-close the send side",
    "files": 6,
    "add": 23,
    "del": 0
   },
   {
    "sha": "b14649f9fe",
    "subject": "http: linger-close after parse errors so clients can read the reply",
    "files": 2,
    "add": 116,
    "del": 21
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "5f354a5834b573d6",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}