Merge #15495: Add regtests for HTTP status codes

8f5d9431a Add regtests for HTTP status codes. (Daniel Kraft)

Pull request description:

  This adds explicit tests for the returned HTTP status codes to `interface_rpc.py` (for error cases) and the HTTP JSON-RPC client in general for success.

  #15381 brought up discussion about the HTTP status codes in general, and the general opinion was that the current choice may not be ideal but should not be changed to preserve compatibility with existing JSON-RPC clients.  Thus it makes sense to actually test the current status to ensure this desired compatibility is not broken accidentally.

ACKs for commit 8f5d94:
  laanwj:
    utACK 8f5d9431a3
  promag:
    utACK 8f5d943.
  jonasschnelli:
    utACK 8f5d9431a3

Tree-SHA512: 82503ccd134dd9145304e95cb6c61755f100bee27593d567cdd5c0c554d47e7b06d937456cab04107f46f4984930355db65d5e711008a0b05f2b8feec9f2950e
This commit is contained in:
Jonas Schnelli 2019-04-08 09:06:31 +02:00
commit 327d2746fb
No known key found for this signature in database
GPG key ID: 1EB776BB03C7922D
2 changed files with 35 additions and 8 deletions

View file

@ -4,9 +4,19 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Tests some generic aspects of the RPC interface.""" """Tests some generic aspects of the RPC interface."""
from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_greater_than_or_equal from test_framework.util import assert_equal, assert_greater_than_or_equal
def expect_http_status(expected_http_status, expected_rpc_code,
fcn, *args):
try:
fcn(*args)
raise AssertionError("Expected RPC error %d, got none" % expected_rpc_code)
except JSONRPCException as exc:
assert_equal(exc.error["code"], expected_rpc_code)
assert_equal(exc.http_status, expected_http_status)
class RPCInterfaceTest(BitcoinTestFramework): class RPCInterfaceTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.num_nodes = 1 self.num_nodes = 1
@ -48,9 +58,16 @@ class RPCInterfaceTest(BitcoinTestFramework):
assert_equal(result_by_id[3]['error'], None) assert_equal(result_by_id[3]['error'], None)
assert result_by_id[3]['result'] is not None assert result_by_id[3]['result'] is not None
def test_http_status_codes(self):
self.log.info("Testing HTTP status codes for JSON-RPC requests...")
expect_http_status(404, -32601, self.nodes[0].invalidmethod)
expect_http_status(500, -8, self.nodes[0].getblockhash, 42)
def run_test(self): def run_test(self):
self.test_getrpcinfo() self.test_getrpcinfo()
self.test_batch_request() self.test_batch_request()
self.test_http_status_codes()
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -35,6 +35,7 @@ ServiceProxy class:
import base64 import base64
import decimal import decimal
from http import HTTPStatus
import http.client import http.client
import json import json
import logging import logging
@ -49,13 +50,14 @@ USER_AGENT = "AuthServiceProxy/0.1"
log = logging.getLogger("BitcoinRPC") log = logging.getLogger("BitcoinRPC")
class JSONRPCException(Exception): class JSONRPCException(Exception):
def __init__(self, rpc_error): def __init__(self, rpc_error, http_status=None):
try: try:
errmsg = '%(message)s (%(code)i)' % rpc_error errmsg = '%(message)s (%(code)i)' % rpc_error
except (KeyError, TypeError): except (KeyError, TypeError):
errmsg = '' errmsg = ''
super().__init__(errmsg) super().__init__(errmsg)
self.error = rpc_error self.error = rpc_error
self.http_status = http_status
def EncodeDecimal(o): def EncodeDecimal(o):
@ -131,19 +133,26 @@ class AuthServiceProxy():
def __call__(self, *args, **argsn): def __call__(self, *args, **argsn):
postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
response = self._request('POST', self.__url.path, postdata.encode('utf-8')) response, status = self._request('POST', self.__url.path, postdata.encode('utf-8'))
if response['error'] is not None: if response['error'] is not None:
raise JSONRPCException(response['error']) raise JSONRPCException(response['error'], status)
elif 'result' not in response: elif 'result' not in response:
raise JSONRPCException({ raise JSONRPCException({
'code': -343, 'message': 'missing JSON-RPC result'}) 'code': -343, 'message': 'missing JSON-RPC result'}, status)
elif status != HTTPStatus.OK:
raise JSONRPCException({
'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status)
else: else:
return response['result'] return response['result']
def batch(self, rpc_call_list): def batch(self, rpc_call_list):
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
log.debug("--> " + postdata) log.debug("--> " + postdata)
return self._request('POST', self.__url.path, postdata.encode('utf-8')) response, status = self._request('POST', self.__url.path, postdata.encode('utf-8'))
if status != HTTPStatus.OK:
raise JSONRPCException({
'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status)
return response
def _get_response(self): def _get_response(self):
req_start_time = time.time() req_start_time = time.time()
@ -162,8 +171,9 @@ class AuthServiceProxy():
content_type = http_response.getheader('Content-Type') content_type = http_response.getheader('Content-Type')
if content_type != 'application/json': if content_type != 'application/json':
raise JSONRPCException({ raise JSONRPCException(
'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}) {'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)},
http_response.status)
responsedata = http_response.read().decode('utf8') responsedata = http_response.read().decode('utf8')
response = json.loads(responsedata, parse_float=decimal.Decimal) response = json.loads(responsedata, parse_float=decimal.Decimal)
@ -172,7 +182,7 @@ class AuthServiceProxy():
log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
else: else:
log.debug("<-- [%.6f] %s" % (elapsed, responsedata)) log.debug("<-- [%.6f] %s" % (elapsed, responsedata))
return response return response, http_response.status
def __truediv__(self, relative_uri): def __truediv__(self, relative_uri):
return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn) return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn)