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:
@@ -35,6 +35,7 @@ from base64 import b64decode, b64encode
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import socket
|
||||
from enum import IntEnum
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web, client_exceptions
|
||||
@@ -44,7 +45,7 @@ from . import util
|
||||
from .network import Network
|
||||
from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare, InvalidPassword)
|
||||
from .invoices import PR_PAID, PR_EXPIRED
|
||||
from .util import log_exceptions, ignore_exceptions, randrange, OldTaskGroup
|
||||
from .util import log_exceptions, ignore_exceptions, randrange, OldTaskGroup, UserFacingException, JsonRPCError
|
||||
from .util import EventListener, event_listener
|
||||
from .wallet import Wallet, Abstract_Wallet
|
||||
from .storage import WalletStorage
|
||||
@@ -251,11 +252,20 @@ class AuthenticatedServer(Logger):
|
||||
response['result'] = await f(**params)
|
||||
else:
|
||||
response['result'] = await f(*params)
|
||||
except UserFacingException as e:
|
||||
response['error'] = {
|
||||
'code': JsonRPCError.Codes.USERFACING,
|
||||
'message': str(e),
|
||||
}
|
||||
except BaseException as e:
|
||||
self.logger.exception("internal error while executing RPC")
|
||||
response['error'] = {
|
||||
'code': 1,
|
||||
'message': str(e),
|
||||
'code': JsonRPCError.Codes.INTERNAL,
|
||||
'message': "internal error while executing RPC",
|
||||
'data': {
|
||||
"exception": repr(e),
|
||||
"traceback": "".join(traceback.format_exception(e)),
|
||||
},
|
||||
}
|
||||
return web.json_response(response)
|
||||
|
||||
@@ -325,12 +335,11 @@ class CommandsServer(AuthenticatedServer):
|
||||
if hasattr(self.daemon.gui_object, 'new_window'):
|
||||
path = config_options.get('wallet_path') or self.config.get_wallet_path(use_gui_last_wallet=True)
|
||||
self.daemon.gui_object.new_window(path, config_options.get('url'))
|
||||
response = "ok"
|
||||
return True
|
||||
else:
|
||||
response = "error: current GUI does not support multiple windows"
|
||||
raise UserFacingException("error: current GUI does not support multiple windows")
|
||||
else:
|
||||
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
||||
return response
|
||||
raise UserFacingException("error: Electrum is running in daemon mode. Please stop the daemon first.")
|
||||
|
||||
async def run_cmdline(self, config_options):
|
||||
cmdname = config_options['cmd']
|
||||
@@ -348,11 +357,8 @@ class CommandsServer(AuthenticatedServer):
|
||||
elif 'wallet' in cmd.options:
|
||||
kwargs['wallet'] = config_options.get('wallet_path')
|
||||
func = getattr(self.cmd_runner, cmd.name)
|
||||
# fixme: not sure how to retrieve message in jsonrpcclient
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
result = {'error':str(e)}
|
||||
# execute requested command now. note: cmd can raise, the caller (self.handle) will wrap it.
|
||||
result = await func(*args, **kwargs)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user