Skip to content

Commit 57351d6

Browse files
committed
fix jsonrpc error handling
1 parent 6e483ed commit 57351d6

3 files changed

Lines changed: 103 additions & 11 deletions

File tree

src/galaxy/api/jsonrpc.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ def anonymise_sensitive_params(params, sensitive_params):
9696

9797
return params
9898

99+
100+
def _iter_error_types(error_type):
101+
yield error_type
102+
for subclass in error_type.__subclasses__():
103+
yield from _iter_error_types(subclass)
104+
105+
106+
def build_error(error):
107+
data = error.setdefault("data", None)
108+
code = error.setdefault("code", 0)
109+
message = error.setdefault("message", "")
110+
111+
if isinstance(data, Mapping):
112+
internal_type = data.get("internal_type")
113+
if internal_type:
114+
for error_type in _iter_error_types(JsonRpcError):
115+
if error_type.__name__ == internal_type:
116+
return error_type(message, data)
117+
118+
return JsonRpcError(code, message, data)
119+
99120
class Connection():
100121
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
101122
self._active = True
@@ -192,6 +213,10 @@ def close(self):
192213
if self._active:
193214
logger.info("Closing JSON-RPC server - not more messages will be read")
194215
self._active = False
216+
for request_id, (future, _) in self._requests_futures.items():
217+
if not future.done():
218+
future.set_exception(Aborted(data={"request_id": request_id}))
219+
self._requests_futures.clear()
195220

196221
async def wait_closed(self):
197222
await self._task_manager.wait()
@@ -216,7 +241,13 @@ def _handle_input(self, data):
216241
self._handle_response(message)
217242

218243
def _handle_response(self, response):
219-
request_future = self._requests_futures.get(int(response.id))
244+
try:
245+
request_id = int(response.id)
246+
except (TypeError, ValueError):
247+
logger.warning("Received response with invalid request id: %s", response.id)
248+
return
249+
250+
request_future = self._requests_futures.pop(request_id, None)
220251
if request_future is None:
221252
response_type = "response" if response.result is not None else "error"
222253
logger.warning("Received %s for unknown request: %s", response_type, response.id)
@@ -225,11 +256,7 @@ def _handle_response(self, response):
225256
future, sensitive_params = request_future
226257

227258
if response.error:
228-
error = JsonRpcError(
229-
response.error.setdefault("code", 0),
230-
response.error.setdefault("message", ""),
231-
response.error.setdefault("data", None)
232-
)
259+
error = build_error(response.error)
233260
self._log_error(response, error, sensitive_params)
234261
future.set_exception(error)
235262
return
@@ -249,7 +276,8 @@ def _handle_notification(self, request):
249276
try:
250277
bound_args = signature.bind(**request.params)
251278
except TypeError:
252-
self._send_error(request.id, InvalidParams())
279+
logger.error("Received notification with invalid params: %s", request.method)
280+
return
253281

254282
if immediate:
255283
callback(*bound_args.args, **bound_args.kwargs)
@@ -273,6 +301,7 @@ def _handle_request(self, request):
273301
bound_args = signature.bind(**request.params)
274302
except TypeError:
275303
self._send_error(request.id, InvalidParams())
304+
return
276305

277306
if immediate:
278307
response = callback(*bound_args.args, **bound_args.kwargs)

tests/test_internal.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from galaxy.api.plugin import Plugin
44
from galaxy.api.consts import Platform
5+
from galaxy.api.jsonrpc import InvalidParams
56
from galaxy.unittest.mock import delayed_return_value_iterable
67

78
from tests import create_message, get_messages
@@ -97,3 +98,41 @@ async def test_tick_after_handshake(plugin, read):
9798
await plugin.run()
9899
await plugin.wait_closed()
99100
plugin.tick.assert_called_with()
101+
102+
103+
@pytest.mark.asyncio
104+
async def test_notification_invalid_params_does_not_crash(plugin, read, write):
105+
request = {
106+
"jsonrpc": "2.0",
107+
"method": "install_game",
108+
"params": {}
109+
}
110+
read.side_effect = [create_message(request), b""]
111+
112+
await plugin.run()
113+
await plugin.wait_closed()
114+
115+
plugin.install_game.assert_not_called()
116+
assert get_messages(write) == []
117+
118+
119+
@pytest.mark.asyncio
120+
async def test_request_invalid_params_returns_error(plugin, read, write):
121+
request = {
122+
"jsonrpc": "2.0",
123+
"id": "8",
124+
"method": "start_achievements_import",
125+
"params": {}
126+
}
127+
read.side_effect = [create_message(request), b""]
128+
129+
await plugin.run()
130+
await plugin.wait_closed()
131+
132+
assert get_messages(write) == [
133+
{
134+
"jsonrpc": "2.0",
135+
"id": "8",
136+
"error": InvalidParams().json()
137+
}
138+
]

tests/test_refresh_credentials.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from galaxy.api.errors import (
66
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
77
)
8-
from galaxy.api.jsonrpc import JsonRpcError
8+
from galaxy.api.jsonrpc import Aborted
99

1010

1111
@pytest.mark.asyncio
@@ -37,6 +37,7 @@ async def test_refresh_credentials_success(plugin, read, write):
3737
]
3838

3939
assert result == refreshed_credentials
40+
assert plugin._connection._requests_futures == {}
4041
await run_task
4142

4243
@pytest.mark.asyncio
@@ -56,11 +57,10 @@ async def test_refresh_credentials_failure(exception, plugin, read, write):
5657
# 2 loop iterations delay is to force sending response after request has been sent
5758
read.side_effect = [create_message(response), b""]
5859

59-
with pytest.raises(JsonRpcError) as e:
60+
with pytest.raises(exception) as e:
6061
await plugin.refresh_credentials({}, False)
6162

62-
# Go back to comparing error == e.value, after fixing current always raising JsonRpcError when handling a response with an error
63-
assert error.code == e.value.code
63+
assert error == e.value
6464
assert get_messages(write) == [
6565
{
6666
"jsonrpc": "2.0",
@@ -70,5 +70,29 @@ async def test_refresh_credentials_failure(exception, plugin, read, write):
7070
"id": "1"
7171
}
7272
]
73+
assert plugin._connection._requests_futures == {}
7374

7475
await run_task
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_refresh_credentials_aborted_on_close(plugin, write):
80+
refresh_task = asyncio.create_task(plugin.refresh_credentials({}, False))
81+
82+
await asyncio.sleep(0)
83+
plugin.close()
84+
85+
with pytest.raises(Aborted):
86+
await refresh_task
87+
88+
assert get_messages(write) == [
89+
{
90+
"jsonrpc": "2.0",
91+
"method": "refresh_credentials",
92+
"params": {
93+
},
94+
"id": "1"
95+
}
96+
]
97+
assert plugin._connection._requests_futures == {}
98+
await plugin.wait_closed()

0 commit comments

Comments
 (0)