http: do not process requests pipelined behind "Connection: close" #36355

pull azuchi wants to merge 1 commits into bitcoin:master from azuchi:http-no-dispatch-after-close changing 4 files +131 −1
  1. azuchi commented at 5:36 AM on September 27, 2026: contributor

    After the reply to a Connection: close request has been queued, TryReadRequest() still parses whatever is left in the receive buffer and hands it to a worker (it only checks m_req_busy), so a request pipelined behind the Connection: close request is executed. Once the reply has been flushed, MaybeSendBytesFromBuffer() flags the client for disconnection and the client is removed at the end of the I/O loop iteration, so the reply to the pipelined request is dropped (m_client.lock() fails in WriteReply()). If the closing reply is not flushed right away (large reply, slow client), the pipelined request is even answered and, since the last reply decides m_keep_alive, a keep-alive request pipelined behind the close request keeps the connection open.

    RFC 9112 section 9.6 requires a server that sends a close response not to process any further requests on that connection. A client that relies on this and resends the request executes it twice, which matters for RPCs with side effects. The libevent based server freed the connection right after writing the close reply (evhttp_send_done() → evhttp_connection_free()), discarding any pipelined bytes, so this is a regression from #35182. It requires an authenticated client that pipelines behind a Connection: close request, so it is a robustness issue rather than a security one.

    The fix records in the client that a closing reply has been queued (m_closing, set in Send() when the reply is not keep-alive) and makes TryReadRequest() stop reading from such a client, as well as from a client already flagged for disconnection (m_disconnect, which also covers permanent send and receive errors). The state machine unit test relied on reading a request pipelined behind an HTTP/1.0 reply without keep-alive; that request is now keep-alive, and two cases are added: a closing reply that is flushed by the optimistic send, and one that stays queued because the socket does not accept data.

    The functional test pipelines a setnetworkactive false request behind a blocking waitforblockheight request that carries Connection: close, generates a block, and checks that the server closed the connection right after the first reply, that only one request from that connection was handed to a worker (the I/O thread logs every request it hands over, with the client's address and port, before it closes the connection), and that the network is still active.

  2. http: do not process requests pipelined behind "Connection: close"
    TryReadRequest() only checked m_req_busy, so after the reply to a
    "Connection: close" request had been queued it still parsed what was
    left in the receive buffer and handed it to a worker. The pipelined
    request was executed, and its reply was dropped once the closing reply
    had been flushed and the client disconnected. A client that resends it,
    as RFC 9112 section 9.6 allows, executes it twice. The libevent based
    server freed the connection right after writing the close reply, so
    this is a regression from the HTTP server rewrite.
    
    Record that a closing reply has been queued and stop reading requests
    from such a client, as well as from one flagged for disconnection.
    Adjust the state machine unit test, which relied on reading a request
    behind an HTTP/1.0 reply, and add unit and functional tests.
    3c37828971
  3. DrahtBot added the label RPC/REST/ZMQ on Sep 27, 2026
  4. DrahtBot commented at 5:36 AM on September 27, 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/36355.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process. A summary of reviews will appear here.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  5. DrahtBot added the label CI failed on Sep 27, 2026
  6. azuchi commented at 6:57 AM on September 27, 2026: contributor

    The CI failure in "Windows, test cross-built" looks unrelated to this PR: https://github.com/bitcoin/bitcoin/actions/runs/36297704387/job/108561658571

    The only failing test is wallet_send.py, in test_maxfeerate (added in #29278):

    File "wallet_send.py", line 225, in test_maxfeerate
        self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), amount=1, fee_rate=Decimal("1.009"))
    test_framework.util.JSONRPCException: Fee rate exceeds maximum configured by user (maxfeerate) (-6)
    

    This PR only touches the HTTP server and its tests, and all other jobs passed. I could not reproduce the failure locally (12 runs of wallet_send.py on the same commit passed), so it seems to be intermittent.

    My guess at the cause, not verified: the test sends with a fee rate exactly equal to -maxfeerate. The fee is computed from the estimated maximum signed size, while the limit is checked against the vsize of the signed transaction, so a signature that is one byte shorter than estimated can push the fee just above the limit.

  7. DrahtBot removed the label CI failed on Sep 28, 2026

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-28 10:51 UTC

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