Merge bitcoin/bitcoin#36118: test: tolerate race condition in interface_http.py
What changed, and why it matters
This commit only changes a test script. It makes the test accept either receiving an HTTP error response or the connection being abruptly closed, because on Windows the Python test client sometimes sees the socket close before it can read the server's error reply. The actual Bitcoin Core server behavior is not changed.
No security action required. This is a test reliability fix. If desired, reviewers can separately consider the suggested SO_LINGER production improvement, but that is not part of this commit.
Security signals we found
No production code changes
Test-only race-condition tolerance
Server still logs HTTP 400 and disconnects misbehaving clients
Malformed request handling behavior unchanged
Evidence from the diff
The patch modifies test/functional/interface_http.py to wrap malformed HTTP request checks in a helper that tolerates both (a) an HTTP 400 Bad Request response and (b) a network-level disconnect/reset before the response is read. The helper asserts that the server still logs the 400 status and that the socket is closed. No production code in src/httpserver.cpp or elsewhere is modified, so the server’s handling of malformed requests remains unchanged.
Changed components
test/functional/interface_http.pyInspect captured patch +24 / −8
### test/functional/interface_http.py
@@ -104,6 +104,26 @@ def setup_network(self):
self.setup_nodes()
self.node = self.nodes[0]
+ def send_bad_and_tolerate_disconnect(self, conn, predicate_fn):
+ '''
+ Tolerate a race condition when sending a malformed request that should result
+ in the server disconnecting the client. The server *should* be sending an error
+ response as well but in some conditions on some platforms (Windows) the python
+ client might encounter the socket error before processing the response.
+ '''
+ with self.node.assert_debug_log([f"HTTPResponse (status code: {http.client.BAD_REQUEST}"]):
+ try:
+ response = predicate_fn()
+ assert_equal(response.status, http.client.BAD_REQUEST)
+ self.log.info(f"Client received expected {http.client.BAD_REQUEST} response before connection was terminated")
+ # Drain server response
+ response.read()
+ conn.set_timeout(2)
+ except NETWORK_ERRORS:
+ self.log.info(f"Client did not receive expected {http.client.BAD_REQUEST} response before connection was terminated")
+ assert conn.sock_closed()
+
+
def run_test(self):
# The test framework typically reuses a single persistent HTTP connection
# for all RPCs to a TestNode. Because we are setting -rpcservertimeout
@@ -192,8 +212,7 @@ def check_excessive_request_size(self):
# Excessive URI size plus default headers breaks the limit.
conn = BitcoinHTTPConnection(self.node)
- response2 = conn.get(f'/{"x" * MAX_HEADERS_SIZE}')
- assert_equal(response2.status, http.client.BAD_REQUEST)
+ self.send_bad_and_tolerate_disconnect(conn, lambda: conn.get(f'/{"x" * MAX_HEADERS_SIZE}'))
# Compute how many short header lines need to be added to http.client
# default headers to make / break the total limit in a single request.
@@ -212,8 +231,7 @@ def check_excessive_request_size(self):
conn = BitcoinHTTPConnection(self.node)
for i in range(headers_above_limit):
conn.add_header(f"header_{i:04}", "foo")
- response3 = conn.get('/x')
- assert_equal(response3.status, http.client.BAD_REQUEST)
+ self.send_bad_and_tolerate_disconnect(conn, lambda: conn.get('/x'))
# Compute how much data we can add to a request message body
# to make / break the limit.
@@ -593,8 +611,7 @@ def check_whitespace_in_headers(self):
# Extra whitespace before colon in header.
conn = BitcoinHTTPConnection(self.node)
conn.headers = {"Authorization ": f"Basic {str_to_b64str(conn.authpair)}"}
- response = conn.post('/', '{"method": "getbestblockhash"}')
- assert_equal(response.status, http.client.BAD_REQUEST)
+ self.send_bad_and_tolerate_disconnect(conn, lambda: conn.post('/', '{"method": "getbestblockhash"}'))
# Extra whitespace at start of new line.
# "line folding" as defined in
@@ -603,8 +620,7 @@ def check_whitespace_in_headers(self):
# https://www.rfc-editor.org/rfc/rfc7230#section-3.2.4
conn = BitcoinHTTPConnection(self.node)
conn.headers = {"Authorization": f"Basic \n {str_to_b64str(conn.authpair)}"}
- response = conn.post('/', '{"method": "getbestblockhash"}')
- assert_equal(response.status, http.client.BAD_REQUEST)
+ self.send_bad_and_tolerate_disconnect(conn, lambda: conn.post('/', '{"method": "getbestblockhash"}'))
def check_connection_limit(self):Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.