diff --git a/contrib/pyln-client/README.md b/contrib/pyln-client/README.md index 9777fd449..2e515eb4c 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 f397fc4bc..87aa68d99 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 @@ __all__ = [ "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 cc2ce1bd3..98d0059a7 100644 --- a/contrib/pyln-client/pyln/client/plugin.py +++ b/contrib/pyln-client/pyln/client/plugin.py @@ -61,6 +61,14 @@ class Method(object): 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 @@ class Request(dict): 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 @@ class Request(dict): "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 5a95ca616..9393269a5 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 test_methods_errors(): 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(