From 85bd9718a1ec2f82d8065066702813e6cc40e7ab Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 15 Dec 2020 15:08:52 +1030 Subject: [PATCH] pyln: add RpcException for finer method failure control. Allows caller to set code and exact message to be returned. Signed-off-by: Rusty Russell Changelog-Added: pyln-client: plugins can now raise RpcException for finer control over error returns. --- contrib/pyln-client/README.md | 3 ++ contrib/pyln-client/pyln/client/__init__.py | 3 +- contrib/pyln-client/pyln/client/plugin.py | 22 ++++++++++--- contrib/pyln-client/tests/test_plugin.py | 35 ++++++++++++++++++++- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/contrib/pyln-client/README.md b/contrib/pyln-client/README.md index 9777fd449214..2e515eb4cd25 100644 --- a/contrib/pyln-client/README.md +++ b/contrib/pyln-client/README.md @@ -78,6 +78,9 @@ def hello(plugin, name="world"): It gets reported as the description when registering the function as a method with `lightningd`. + If this returns (a dict), that's the JSON "result" returned. If + it raises an exception, that causes a JSON "error" return (raising + pyln.client.RpcException allows finer control over the return). """ greeting = plugin.get_option('greeting') s = '{} {}'.format(greeting, name) diff --git a/contrib/pyln-client/pyln/client/__init__.py b/contrib/pyln-client/pyln/client/__init__.py index f397fc4bcb66..87aa68d9988a 100644 --- a/contrib/pyln-client/pyln/client/__init__.py +++ b/contrib/pyln-client/pyln/client/__init__.py @@ -1,5 +1,5 @@ from .lightning import LightningRpc, RpcError, Millisatoshi -from .plugin import Plugin, monkey_patch +from .plugin import Plugin, monkey_patch, RpcException __version__ = "0.8.0" @@ -9,6 +9,7 @@ "LightningRpc", "Plugin", "RpcError", + "RpcException", "Millisatoshi", "__version__", "monkey_patch" diff --git a/contrib/pyln-client/pyln/client/plugin.py b/contrib/pyln-client/pyln/client/plugin.py index cc2ce1bd3530..98d0059a742e 100644 --- a/contrib/pyln-client/pyln/client/plugin.py +++ b/contrib/pyln-client/pyln/client/plugin.py @@ -61,6 +61,14 @@ def __init__(self, name: str, func: Callable[..., JSONType], self.after: List[str] = [] +class RpcException(Exception): + # -32600 == "Invalid Request" + def __init__(self, message: str, code: int = -32600): + self.code = code + self.message = message + super().__init__("RpcException: {}".format(message)) + + class Request(dict): """A request object that wraps params and allows async return """ @@ -102,7 +110,7 @@ def set_result(self, result: Any) -> None: self.state = RequestState.FINISHED self.termination_tb = "".join(traceback.extract_stack().format()[:-1]) - def set_exception(self, exc: Exception) -> None: + def set_exception(self, exc: Union[Exception, RpcException]) -> None: if self.state != RequestState.PENDING: assert(self.termination_tb is not None) raise ValueError( @@ -110,13 +118,19 @@ def set_exception(self, exc: Exception) -> None: "current state is {state}. Request previously terminated at\n" "{tb}".format(state=self.state, tb=self.termination_tb)) self.exc = exc + if isinstance(exc, RpcException): + code = exc.code + message = exc.message + else: + code = -32600 # "Invalid Request" + message = ("Error while processing {method}: {exc}" + .format(method=self.method, exc=str(exc))) self._write_result({ 'jsonrpc': '2.0', 'id': self.id, "error": { - "code": -32600, # "Invalid Request" - "message": "Error while processing {method}: {exc}" - .format(method=self.method, exc=str(exc)), + "code": code, + "message": message, # 'data' field "may be omitted." "traceback": traceback.format_exc(), }, diff --git a/contrib/pyln-client/tests/test_plugin.py b/contrib/pyln-client/tests/test_plugin.py index 5a95ca616f4f..9393269a5efd 100644 --- a/contrib/pyln-client/tests/test_plugin.py +++ b/contrib/pyln-client/tests/test_plugin.py @@ -1,5 +1,5 @@ from pyln.client import Plugin -from pyln.client.plugin import Request, Millisatoshi +from pyln.client.plugin import Request, Millisatoshi, RpcException import itertools import pytest # type: ignore @@ -172,6 +172,39 @@ def test1(name): assert call_list == [] +def test_method_exceptions(): + """A bunch of tests that should fail calling the methods.""" + p = Plugin(autopatch=False) + + def fake_write_result(resultdict): + global result_dict + result_dict = resultdict + + @p.method("test_raise") + def test_raise(): + raise RpcException("testing RpcException", code=-1000) + + req = Request(p, 1, "test_raise", {}) + req._write_result = fake_write_result + p._dispatch_request(req) + assert result_dict['jsonrpc'] == '2.0' + assert result_dict['id'] == 1 + assert result_dict['error']['code'] == -1000 + assert result_dict['error']['message'] == "testing RpcException" + + @p.method("test_raise2") + def test_raise2(): + raise Exception("normal exception") + + req = Request(p, 1, "test_raise2", {}) + req._write_result = fake_write_result + p._dispatch_request(req) + assert result_dict['jsonrpc'] == '2.0' + assert result_dict['id'] == 1 + assert result_dict['error']['code'] == -32600 + assert result_dict['error']['message'] == "Error while processing test_raise2: normal exception" + + def test_positional_inject(): p = Plugin() rdict = Request(