cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.
With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).
As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.
----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
from electrum.util import UserFacingException
raise UserFacingException("heyheyhey")
@command('')
async def errorbad(self):
raise Exception("heyheyhey")
```
----------
(before change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```
----------
(after change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
response['result'] = await f(*params)
File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
result = fut.result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n response['result'] = await f(*params)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n return await func(*args, **kwargs)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
This commit is contained in:
36
run_electrum
36
run_electrum
@@ -104,7 +104,7 @@ from electrum.util import InvalidPassword
|
||||
from electrum.commands import get_parser, known_commands, Commands, config_variables
|
||||
from electrum import daemon
|
||||
from electrum import keystore
|
||||
from electrum.util import create_and_start_event_loop
|
||||
from electrum.util import create_and_start_event_loop, UserFacingException, JsonRPCError
|
||||
from electrum.i18n import set_language
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -462,7 +462,17 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict):
|
||||
else:
|
||||
sys_exit(0)
|
||||
else:
|
||||
result = daemon.request(config, 'gui', (config_options,))
|
||||
try:
|
||||
result = daemon.request(config, 'gui', (config_options,))
|
||||
except JsonRPCError as e:
|
||||
if e.code == JsonRPCError.Codes.USERFACING:
|
||||
print_stderr(e.message)
|
||||
elif e.code == JsonRPCError.Codes.INTERNAL:
|
||||
print_stderr("(inside daemon): " + e.data["traceback"])
|
||||
print_stderr(e.message)
|
||||
else:
|
||||
raise Exception(f"unknown error code {e.code}")
|
||||
sys_exit(1)
|
||||
|
||||
elif cmdname == 'daemon':
|
||||
|
||||
@@ -497,8 +507,17 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict):
|
||||
print_msg("Found lingering lockfile for daemon. Removing.")
|
||||
daemon.remove_lockfile(lockfile)
|
||||
sys_exit(1)
|
||||
except JsonRPCError as e:
|
||||
if e.code == JsonRPCError.Codes.USERFACING:
|
||||
print_stderr(e.message)
|
||||
elif e.code == JsonRPCError.Codes.INTERNAL:
|
||||
print_stderr("(inside daemon): " + e.data["traceback"])
|
||||
print_stderr(e.message)
|
||||
else:
|
||||
raise Exception(f"unknown error code {e.code}")
|
||||
sys_exit(1)
|
||||
except Exception as e:
|
||||
print_stderr(str(e) or repr(e))
|
||||
_logger.exception("error running command (with daemon)")
|
||||
sys_exit(1)
|
||||
else:
|
||||
if cmd.requires_network:
|
||||
@@ -520,14 +539,15 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict):
|
||||
finally:
|
||||
plugins.stop()
|
||||
plugins.stopped_event.wait(1)
|
||||
except Exception as e:
|
||||
print_stderr(str(e) or repr(e))
|
||||
except UserFacingException as e:
|
||||
print_stderr(str(e))
|
||||
sys_exit(1)
|
||||
except Exception as e:
|
||||
_logger.exception("error running command (without daemon)")
|
||||
sys_exit(1)
|
||||
# print result
|
||||
if isinstance(result, str):
|
||||
print_msg(result)
|
||||
elif type(result) is dict and result.get('error'):
|
||||
print_stderr(result.get('error'))
|
||||
sys_exit(1)
|
||||
elif result is not None:
|
||||
print_msg(json_encode(result))
|
||||
sys_exit(0)
|
||||
|
||||
Reference in New Issue
Block a user