What changed, and why it matters
This commit only changes a single test file (interface_http.py) in Bitcoin Core. It refactors the test to use one node instead of three, switches some encodings from utf-8 to ascii, makes tests independent, and updates expected HTTP error codes from 400 to 413 for oversized requests. There is no change to production code, no security fix, and no vulnerability being introduced.
No security action required. Treat as routine test-code cleanup.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff is entirely within test/functional/interface_http.py. It is a test-maintenance follow-up: reduces self.num_nodes from 3 to 1, stores self.node = self.nodes[0], moves reuse_http_connections setup earlier, changes post_raw to encode headers as ascii and body as utf-8 bytes with correct Content-Length, updates oversized request assertions from http.client.BAD_REQUEST (400) to http.client.REQUEST_ENTITY_TOO_LARGE (413), and hardens race-condition handling when the server disconnects early. No C++ node code, RPC server, or consensus logic is modified.
Changed components
test/functional/interface_http.pyInspect captured patch +66 / −53
diff --git a/test/functional/interface_http.py b/test/functional/interface_http.py
index ff6d70a0..cfa111b7 100755
--- a/test/functional/interface_http.py
+++ b/test/functional/interface_http.py
@@ -64,11 +64,11 @@ class BitcoinHTTPConnection:
return self._request('GET', path, '', connection_header)
def post_raw(self, path, data):
+ data_bytes = data.encode("utf-8")
req = f"POST {path} HTTP/1.1\r\n"
req += f'Authorization: Basic {str_to_b64str(self.authpair)}\r\n'
- req += f'Content-Length: {len(data)}\r\n\r\n'
- req += data
- self.conn.sock.sendall(req.encode("utf-8"))
+ req += f'Content-Length: {len(data_bytes)}\r\n\r\n'
+ self.conn.sock.sendall(req.encode("ascii") + data_bytes)
def recv_raw(self):
'''
@@ -94,13 +94,22 @@ class BitcoinHTTPConnection:
class HTTPBasicsTest (BitcoinTestFramework):
def set_test_params(self):
- self.num_nodes = 3
+ self.num_nodes = 1
self.supports_cli = False
def setup_network(self):
self.setup_nodes()
+ self.node = self.nodes[0]
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
+ # so low on this one node, its connection will quickly timeout and get dropped by
+ # the server. Negating this setting will force the AuthServiceProxy
+ # for this node to create a fresh new HTTP connection for every command
+ # called for the remainder of this test.
+ self.node.reuse_http_connections = False
+
self.check_default_connection()
self.check_keepalive_connection()
self.check_close_connection()
@@ -113,7 +122,7 @@ class HTTPBasicsTest (BitcoinTestFramework):
def check_default_connection(self):
self.log.info("Checking default HTTP/1.1 connection persistence")
- conn = BitcoinHTTPConnection(self.nodes[0])
+ conn = BitcoinHTTPConnection(self.node)
# Make request without explicit "Connection" header
response1 = conn.post('/', '{"method": "getbestblockhash"}').read()
assert b'"error":null' in response1
@@ -131,7 +140,7 @@ class HTTPBasicsTest (BitcoinTestFramework):
def check_keepalive_connection(self):
self.log.info("Checking keep-alive connection persistence")
- conn = BitcoinHTTPConnection(self.nodes[0])
+ conn = BitcoinHTTPConnection(self.node)
# Make request with explicit "Connection: keep-alive" header
response1 = conn.post('/', '{"method": "getbestblockhash"}', connection_header='keep-alive').read()
assert b'"error":null' in response1
@@ -149,7 +158,7 @@ class HTTPBasicsTest (BitcoinTestFramework):
def check_close_connection(self):
self.log.info("Checking close connection after response")
- conn = BitcoinHTTPConnection(self.nodes[0])
+ conn = BitcoinHTTPConnection(self.node)
# Make request with explicit "Connection: close" header
response1 = conn.post('/', '{"method": "getbestblockhash"}', connection_header='close').read()
assert b'"error":null' in response1
@@ -162,12 +171,12 @@ class HTTPBasicsTest (BitcoinTestFramework):
# Large URI plus up to 1000 bytes of default headers
# added by python's http.client still below total limit.
- conn = BitcoinHTTPConnection(self.nodes[0])
+ conn = BitcoinHTTPConnection(self.node)
response1 = conn.get(f'/{"x" * (MAX_HEADERS_SIZE - 1000)}')
assert_equal(response1.status, http.client.NOT_FOUND)
# Excessive URI size plus default headers breaks the limit.
- conn = BitcoinHTTPConnection(self.nodes[1])
+ conn = BitcoinHTTPConnection(self.node)
response2 = conn.get(f'/{"x" * MAX_HEADERS_SIZE}')
assert_equal(response2.status, http.client.BAD_REQUEST)
@@ -183,14 +192,14 @@ class HTTPBasicsTest (BitcoinTestFramework):
headers_above_limit += 1000 // header_line_length
# Many small header lines is ok
- conn = BitcoinHTTPConnection(self.nodes[2])
+ conn = BitcoinHTTPConnection(self.node)
for i in range(headers_below_limit):
conn.add_header(f"header_{i:04}", "foo")
response3 = conn.get('/x')
assert_equal(response3.status, http.client.NOT_FOUND)
# Too many small header lines exceeds total headers size allowed
- conn = BitcoinHTTPConnection(self.nodes[2])
+ conn = BitcoinHTTPConnection(self.node)
for i in range(headers_above_limit):
conn.add_header(f"header_{i:04}", "foo")
response3 = conn.get('/x')
@@ -203,24 +212,29 @@ class HTTPBasicsTest (BitcoinTestFramework):
bytes_above_limit = MAX_SIZE - base_request_body_size + 2
# Large request body size is ok
- conn = BitcoinHTTPConnection(self.nodes[0])
+ conn = BitcoinHTTPConnection(self.node)
response4 = conn.post('/', f'{{"jsonrpc": "2.0", "id": "0", "method": "submitblock", "params": ["{"0" * bytes_below_limit}"]}}')
assert_equal(response4.status, http.client.OK)
- conn = BitcoinHTTPConnection(self.nodes[1])
+ conn = BitcoinHTTPConnection(self.node)
try:
# Excessive body size is invalid
- response5 = conn.post('/', f'{{"jsonrpc": "2.0", "id": "0", "method": "submitblock", "params": ["{"0" * bytes_above_limit}"]}}')
+ conn.post_raw('/', f'{{"jsonrpc": "2.0", "id": "0", "method": "submitblock", "params": ["{"0" * bytes_above_limit}"]}}')
+ self.log.info("Client finished sending request before connection was terminated")
+ except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
+ self.log.info("Client did not finish sending request before connection was terminated")
- # The server will send a 400 response and disconnect but
- # due to a race condition, the python client may or may not
- # receive the response before detecting the broken socket.
- response5.read()
- assert_equal(response5.status, http.client.BAD_REQUEST)
+ # The server will send a 413 response and disconnect but due to a race
+ # condition, the python client may or may not read the response before
+ # detecting the broken socket (which it may still be trying to write to).
+ try:
+ response5 = conn.conn.getresponse()
+ assert_equal(response5.status, http.client.REQUEST_ENTITY_TOO_LARGE)
+ self.log.info(f"Client got expected response status {response5.status}")
assert conn.sock_closed()
- self.log.debug("Server sent response before terminating connection")
- except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
- self.log.debug("Server terminated connection immediately")
+ # macos/linux windows
+ except (http.client.ResponseNotReady, ConnectionAbortedError):
+ self.log.info("Client did not read response before disconnecting")
def check_pipelining(self):
@@ -229,8 +243,8 @@ class HTTPBasicsTest (BitcoinTestFramework):
See https://www.rfc-editor.org/rfc/rfc7230#section-6.3.2
"""
self.log.info("Check pipelining")
- tip_height = self.nodes[2].getblockcount()
- conn = BitcoinHTTPConnection(self.nodes[2])
+ tip_height = self.node.getblockcount()
+ conn = BitcoinHTTPConnection(self.node)
conn.set_timeout(5)
# Send two requests in a row.
@@ -248,7 +262,7 @@ class HTTPBasicsTest (BitcoinTestFramework):
pass
# Use a separate http connection to generate a block
- self.generate(self.nodes[2], 1, sync_fun=self.no_op)
+ self.generate(self.node, 1, sync_fun=self.no_op)
# Wait for two responses to be received
res = b""
@@ -264,7 +278,7 @@ class HTTPBasicsTest (BitcoinTestFramework):
def check_chunked_transfer(self):
self.log.info("Check HTTP request encoded with chunked transfer")
- conn = BitcoinHTTPConnection(self.nodes[2])
+ conn = BitcoinHTTPConnection(self.node)
headers_chunked = conn.headers.copy()
headers_chunked.update({"Transfer-encoding": "chunked"})
body_chunked = [
@@ -285,7 +299,7 @@ class HTTPBasicsTest (BitcoinTestFramework):
assert b'{"result":"high-hash","error":null}\n' in response1
self.log.info("Check excessive size HTTP request encoded with chunked transfer")
- conn = BitcoinHTTPConnection(self.nodes[0])
+ conn = BitcoinHTTPConnection(self.node)
headers_chunked = conn.headers.copy()
headers_chunked.update({"Transfer-encoding": "chunked"})
body_chunked = [
@@ -303,49 +317,45 @@ class HTTPBasicsTest (BitcoinTestFramework):
body=iter(body_chunked),
headers=headers_chunked,
encode_chunked=True)
+ self.log.info("Client finished sending request before connection was terminated")
+ except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
+ self.log.info("Client did not finish sending request before connection was terminated")
- # The server will send a 400 response and disconnect but
- # due to a race condition, the python client may or may not
- # receive the response before detecting the broken socket.
+ # The server will send a 413 response and disconnect but due to a race
+ # condition, the python client may or may not read the response before
+ # detecting the broken socket (which it may still be trying to write to).
+ try:
response2 = conn.conn.getresponse()
- response2.read()
- assert_equal(response2.status, http.client.BAD_REQUEST)
+ assert_equal(response2.status, http.client.REQUEST_ENTITY_TOO_LARGE)
+ self.log.info(f"Client got expected response status {response2.status}")
assert conn.sock_closed()
- self.log.debug("Server sent response before terminating connection")
- except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
- # ...or just immediately disconnect
- self.log.debug("Server terminated connection immediately")
+ # macos/linux windows
+ except (http.client.ResponseNotReady, ConnectionAbortedError):
+ self.log.info("Client did not read response before disconnecting")
def check_idle_timeout(self):
self.log.info("Check -rpcservertimeout")
- # The test framework typically reuses a single persistent HTTP connection
- # for all RPCs to a TestNode. Because we are setting -rpcservertimeout
- # so low on this one node, its connection will quickly timeout and get dropped by
- # the server. Negating this setting will force the AuthServiceProxy
- # for this node to create a fresh new HTTP connection for every command
- # called for the remainder of this test.
- self.nodes[2].reuse_http_connections = False
# This is the amount of time the server will wait for a client to
# send a complete request. Test it by sending an incomplete but
- # so-far otherwise well-formed HTTP request, and never finishing it
- self.restart_node(2, extra_args=[f"-rpcservertimeout={RPCSERVERTIMEOUT}"])
+ # so-far otherwise well-formed HTTP request, and never finishing it.
+ self.restart_node(0, extra_args=[f"-rpcservertimeout={RPCSERVERTIMEOUT}"])
# Copied from http_incomplete_test_() in regress_http.c in libevent.
# A complete request would have an additional "\r\n" at the end.
bad_http_request = "GET /test1 HTTP/1.1\r\nHost: somehost\r\n"
- conn = BitcoinHTTPConnection(self.nodes[2])
- conn.conn.sock.sendall(bad_http_request.encode("utf-8"))
+ conn = BitcoinHTTPConnection(self.node)
+ conn.conn.sock.sendall(bad_http_request.encode("ascii"))
conn.expect_timeout(RPCSERVERTIMEOUT)
# Sanity check -- complete requests don't timeout waiting for completion
good_http_request = "GET /test2 HTTP/1.1\r\nHost: somehost\r\n\r\n"
conn.reset_conn()
- conn.conn.sock.sendall(good_http_request.encode("utf-8"))
- response2 = conn.recv_raw()
- assert response2.startswith(b"HTTP/1.1 404 Not Found")
+ conn.conn.sock.sendall(good_http_request.encode("ascii"))
+ response = conn.recv_raw()
+ assert response.startswith(b"HTTP/1.1 404 Not Found")
# Still open
assert not conn.sock_closed()
@@ -353,13 +363,16 @@ class HTTPBasicsTest (BitcoinTestFramework):
def check_server_busy_idle_timeout(self):
self.log.info("Check that -rpcservertimeout won't close on a delayed response")
- tip_height = self.nodes[2].getblockcount()
- conn = BitcoinHTTPConnection(self.nodes[2])
+
+ self.restart_node(0, extra_args=[f"-rpcservertimeout={RPCSERVERTIMEOUT}"])
+
+ tip_height = self.node.getblockcount()
+ conn = BitcoinHTTPConnection(self.node)
conn.post_raw('/', f'{{"method": "waitforblockheight", "params": [{tip_height + 1}]}}')
# Wait until after the timeout, then generate a block with a second HTTP connection
time.sleep(RPCSERVERTIMEOUT + 1)
- generated_block = self.generate(self.nodes[2], 1, sync_fun=self.no_op)[0]
+ generated_block = self.generate(self.node, 1, sync_fun=self.no_op)[0]
# The first connection gets the response it is patiently waiting for
response1 = conn.recv_raw().decode()
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.