Repository: bitcoin/bitcoin
File: test/functional/interface_rpc.py (Python)

Code before:
                       fcn, *args):
    try:
        fcn(*args)
        raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none")
    except JSONRPCException as exc:
        assert_equal(exc.error["code"], expected_rpc_code)
        assert_equal(exc.http_status, expected_http_status)


def test_work_queue_getblock(node, got_exceeded_error):
    while not got_exceeded_error:
        try:
            node.cli("waitfornewblock", "500").send_cli()
        except subprocess.CalledProcessError as e:
            assert_equal(e.output, 'error: Server response: Work queue depth exceeded\n')
            got_exceeded_error.append(True)


class RPCInterfaceTest(BitcoinTestFramework):
    def set_test_params(self):
        self.num_nodes = 1
        self.setup_clean_chain = True
        self.supports_cli = False

    def test_getrpcinfo(self):
        self.log.info("Testing getrpcinfo...")

        info = self.nodes[0].getrpcinfo()
        assert_equal(len(info['active_commands']), 1)

        command = info['active_commands'][0]
        assert_equal(command['method'], 'getrpcinfo')
        assert_greater_than_or_equal(command['duration'], 0)
        assert_equal(info['logpath'], os.path.join(self.nodes[0].chain_path, 'debug.log'))

    def test_batch_request(self):
        self.log.info("Testing basic JSON-RPC batch request...")

        results = self.nodes[0].batch([
            # A basic request that will work fine.
            {"method": "getblockcount", "id": 1},
            # Request that will fail.  The whole batch request should still
            # work fine.
            {"method": "invalidmethod", "id": 2},
            # Another call that should succeed.
            {"method": "getblockhash", "id": 3, "params": [0]},
        ])

        result_by_id = {}
        for res in results:
            result_by_id[res["id"]] = res

Review comment (bug):
In commit "test: cover more HTTP error codes in interface_rpc.py" (29ebb0d7774ee2d10aa274086e8ff4b2395f7047)

Setting params to None seems like a bug because it means a `"params": null` JSON field will be sent, when previously authproxy would send an empty params object. Would suggest changing this to `params={}` to avoid a change in behavior.

Also, I'm not sure if it is a bug for our server to accept null params. The spec says "If present, parameters for the rpc call MUST be provided as a Structured value" and "params MUST be an Array" or "params MUST be an Object", so it seems like null is not supposed to be allowed, and probably should trigger a server error.

Code after:

RPC_INVALID_ADDRESS_OR_KEY = -5
RPC_INVALID_PARAMETER      = -8
RPC_METHOD_NOT_FOUND       = -32601
RPC_INVALID_REQUEST        = -32600
RPC_PARSE_ERROR            = -32700


@dataclass
class BatchOptions:
    version: Optional[int] = None
    notification: bool = False
    request_fields: Optional[dict] = None
    response_fields: Optional[dict] = None


def format_request(options, idx, fields):
    request = {}
    if options.version == 1:
        request.update(version="1.1")
    elif options.version == 2:
        request.update(jsonrpc="2.0")
    elif options.version is not None:
        raise NotImplementedError(f"Unknown JSONRPC version {options.version}")
    if not options.notification:
        request.update(id=idx)
    request.update(fields)
    if options.request_fields:
        request.update(options.request_fields)
    return request


def format_response(options, idx, fields):
    if options.version == 2 and options.notification:
        return None
    response = {}
    if not options.notification:
        response.update(id=idx)
    if options.version == 2:
        response.update(jsonrpc="2.0")
    else:
        response.update(result=None, error=None)
    response.update(fields)
    if options.response_fields:
        response.update(options.response_fields)
    return response


def send_raw_rpc(node, raw_body: bytes) -> tuple[object, int]:
    return node._request("POST", "/", raw_body)