HTTPRemoteClient::Send() sets m_keep_alive before taking m_send_mutex and appending the response to the send buffer, while the I/O thread reads it under m_send_mutex in MaybeSendBytesFromBuffer() right after the buffer drains to empty. The two updates are therefore not ordered with respect to each other, and a pipelining client can lose a response:
- Response 1 (keep-alive) is partially sent and the I/O thread is still draining it.
- Request 2 (
Connection: close) is parsed from the receive buffer and dispatched to a worker (the send throttle from #36174 only holds a request back once the buffer exceedsMAX_BODY_SIZE). - The worker clears
m_keep_alive; the I/O thread then finishes sending response 1, sees an empty buffer withm_keep_alive == false, and setsm_disconnect. - The worker appends response 2, which is dropped when the client is disconnected on the next loop iteration.
This PR moves the assignment into the critical section that appends the response, and turns the field from std::atomic_bool into a plain bool GUARDED_BY(m_send_mutex) so Clang's thread-safety analysis enforces the ordering (the only reader already holds the mutex). With the flag and the buffer updated together, the I/O thread can no longer observe "empty && !keep_alive" between two pipelined responses.
The ordering dates back to the initial HTTP server implementation (#35182); #35829/#36007 only moved the code. The window is small and bitcoin-cli does not pipeline, so this is a low-severity fix.
Test
http_pipelined_keepalive_close_tests reproduces the interleaving deterministically with a mock socket: Send() fails with EAGAIN until the second request has been dispatched, then the I/O thread's next Send() (made while holding m_send_mutex) signals the worker to write its reply, waits for it to reach HTTPRemoteClient::Send(), and only then flushes the first reply. Sends are refused again until the test releases them, so the second reply is only delivered if the I/O loop kept the connection open. Without the first commit the test fails with server.GetConnectionsCount() == 1 has failed [0 != 1]: the client was disconnected with the second reply still in its send buffer.