From 87141598916f9f64a598ba7bcd82ea2244a56d54 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sat, 3 Jul 2021 21:26:55 +1000 Subject: [PATCH 1/2] Allow raising exception to return an error --- jsonrpcserver/dispatcher.py | 44 +++++++++++++++++++++---------------- tests/test_dispatcher.py | 20 +++++++++++++++++ 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/jsonrpcserver/dispatcher.py b/jsonrpcserver/dispatcher.py index 5cea40a..bbbfd31 100644 --- a/jsonrpcserver/dispatcher.py +++ b/jsonrpcserver/dispatcher.py @@ -19,18 +19,18 @@ from jsonschema.validators import validator_for # type: ignore from pkg_resources import resource_string # type: ignore +from .exceptions import JsonRpcError from .methods import Methods, global_methods, validate_args from .request import Request, NOID from .response import ( InvalidRequestResponse, - MethodNotFoundResponse, ParseErrorResponse, Response, ServerErrorResponse, from_result, to_serializable, ) -from .result import InvalidParams, InternalError, Error, Result, Success +from .result import InvalidParams, InternalError, MethodNotFound, Error, Result, Success default_deserializer = json.loads @@ -46,7 +46,7 @@ config.read([".jsonrpcserverrc", os.path.expanduser("~/.jsonrpcserverrc")]) -def call(method: Callable, args: list, kwargs: dict) -> Result: +def call(methods: Methods, method_name: str, args: list, kwargs: dict) -> Result: """ Calls a method. @@ -55,20 +55,28 @@ def call(method: Callable, args: list, kwargs: dict) -> Result: Returns: The Result from the method call. """ + try: + method = methods.items[method_name] + except KeyError: + return MethodNotFound(method_name) + errors = validate_args(method, *args, **kwargs) if errors: return InvalidParams(errors) try: result = method(*args, **kwargs) + except JsonRpcError as exc: + return Error(code=exc.code, message=exc.message, data=exc.data) + except Exception as exc: # Other error inside method - server error + logging.exception(exc) + return InternalError(str(exc)) + else: return ( InternalError("The method did not return a Result") if not isinstance(result, (Success, Error)) else result ) - except Exception as exc: # Other error inside method - server error - logging.exception(exc) - return InternalError(str(exc)) def extract_args(request: Request, context: Any) -> list: @@ -90,15 +98,13 @@ def dispatch_request( Converts the return value (a Result) into a Response. """ - if request.method in methods.items: - result = call( - methods.items[request.method], - extract_args(request, context), - extract_kwargs(request), - ) - return None if request.id is NOID else from_result(result, request.id) - else: - return MethodNotFoundResponse(request.method, request.id) + result = call( + methods, + request.method, + extract_args(request, context), + extract_kwargs(request), + ) + return None if request.id is NOID else from_result(result, request.id) def none_if_empty(x: Any) -> Any: @@ -169,8 +175,8 @@ def dispatch_to_response_pure( testing, not dispatch_to_response or dispatch. Args: - deserializer: Function that is used to deserialize data. - schema_validator: + deserializer: Function that deserializes the JSON-RPC request. + schema_validator: Function that validates the JSON-RPC request. context: Will be passed to methods as the first param if not None. methods: Collection of methods that can be called. request: The incoming request string. @@ -223,8 +229,8 @@ def dispatch_to_response( internal, global methods object which is populated with the @method decorator. context: Will be passed to methods as the first param if not None. - schema_validator: - deserializer: Function that is used to deserialize data. + schema_validator: Function that validates the JSON-RPC request. + deserializer: Function that deserializes the JSON-RPC request. Returns: A Response, list of Responses or None. diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 0238aab..bd2bee0 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -11,6 +11,7 @@ dispatch_to_response, dispatch_to_response_pure, ) +from jsonrpcserver.exceptions import JsonRpcError from jsonrpcserver.methods import Methods, global_methods from jsonrpcserver.request import Request, NOID from jsonrpcserver.response import ErrorResponse, SuccessResponse @@ -231,6 +232,25 @@ def not_a_result(): assert response.data == "The method did not return a Result" +def test_dispatch_to_response_pure_raising_exception(): + """Allow raising an exception to return an error.""" + + def raise_exception(): + raise JsonRpcError(code=0, message="foo", data="bar") + + response = dispatch_to_response_pure( + deserializer=default_deserializer, + schema_validator=default_schema_validator, + context=None, + methods=Methods(raise_exception), + request='{"jsonrpc": "2.0", "method": "raise_exception", "id": 1}', + ) + assert isinstance(response, ErrorResponse) + assert response.code == 0 + assert response.message == "foo" + assert response.data == "bar" + + # dispatch_to_response From 55083fa1d5b155c63d9fb499dc7314b740c0e4a5 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sat, 3 Jul 2021 21:33:19 +1000 Subject: [PATCH 2/2] Add exceptions.py --- jsonrpcserver/exceptions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 jsonrpcserver/exceptions.py diff --git a/jsonrpcserver/exceptions.py b/jsonrpcserver/exceptions.py new file mode 100644 index 0000000..f3d4b1b --- /dev/null +++ b/jsonrpcserver/exceptions.py @@ -0,0 +1,12 @@ +""" +An exception can be raised from inside a method to return an error response. + +This is an alternative to returning a Result from the method. + +See https://github.com/bcb/jsonrpcserver/discussions/158 +""" + + +class JsonRpcError(Exception): + def __init__(self, code: int, message: str, data: str): + self.code, self.message, self.data = (code, message, data)