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.