Skip to content

Commit 7c64515

Browse files
authored
Enforce returning a Result from methods (#176)
1 parent 96e72a4 commit 7c64515

File tree

5 files changed

+66
-33
lines changed

5 files changed

+66
-33
lines changed

jsonrpcserver/dispatcher.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from_result,
2626
to_serializable,
2727
)
28-
from .result import InvalidParams, InternalError, Result
28+
from .result import InvalidParams, InternalError, Error, Result, Success
2929

3030
default_deserializer = json.loads
3131

@@ -55,7 +55,12 @@ def call(method: Callable, args: list, kwargs: dict) -> Result:
5555
return InvalidParams(errors)
5656

5757
try:
58-
return method(*args, **kwargs)
58+
result = method(*args, **kwargs)
59+
return (
60+
InternalError("The method did not return a Result")
61+
if not isinstance(result, (Success, Error))
62+
else result
63+
)
5964
except Exception as exc: # Other error inside method - server error
6065
logging.exception(exc)
6166
return InternalError(str(exc))

jsonrpcserver/response.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@
1616
"""
1717
from typing import Any, List, NamedTuple, Union
1818

19-
from . import status
2019
from .request import NOID
2120
from .result import Result, Success, UNSPECIFIED
21+
from .status import (
22+
ERROR_INVALID_REQUEST,
23+
ERROR_METHOD_NOT_FOUND,
24+
ERROR_PARSE_ERROR,
25+
ERROR_SERVER_ERROR,
26+
)
2227

2328

2429
class SuccessResponse(NamedTuple):
@@ -48,7 +53,7 @@ def ParseErrorResponse(data: Any) -> ErrorResponse:
4853
the id member in the Request Object. If there was an error in detecting the id in
4954
the Request object (e.g. Parse error/Invalid Request), it MUST be Null."
5055
"""
51-
return ErrorResponse(status.JSONRPC_PARSE_ERROR_CODE, "Parse error", data, None)
56+
return ErrorResponse(ERROR_PARSE_ERROR, "Parse error", data, None)
5257

5358

5459
def InvalidRequestResponse(data: Any) -> ErrorResponse:
@@ -57,19 +62,15 @@ def InvalidRequestResponse(data: Any) -> ErrorResponse:
5762
the id member in the Request Object. If there was an error in detecting the id in
5863
the Request object (e.g. Parse error/Invalid Request), it MUST be Null."
5964
"""
60-
return ErrorResponse(
61-
status.JSONRPC_INVALID_REQUEST_CODE, "Invalid request", data, None
62-
)
65+
return ErrorResponse(ERROR_INVALID_REQUEST, "Invalid request", data, None)
6366

6467

6568
def MethodNotFoundResponse(data: Any, id: Any) -> ErrorResponse:
66-
return ErrorResponse(
67-
status.JSONRPC_METHOD_NOT_FOUND_CODE, "Method not found", data, id
68-
)
69+
return ErrorResponse(ERROR_METHOD_NOT_FOUND, "Method not found", data, id)
6970

7071

7172
def ServerErrorResponse(data: Any, id: Any) -> ErrorResponse:
72-
return ErrorResponse(status.JSONRPC_SERVER_ERROR_CODE, "Server error", data, id)
73+
return ErrorResponse(ERROR_SERVER_ERROR, "Server error", data, id)
7374

7475

7576
def from_result(result: Result, id: Any) -> Union[Response, None]:

jsonrpcserver/result.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def my_method(request: Request) -> Result:
2525
"""
2626
from typing import Any, NamedTuple, Optional, Union
2727

28-
from . import status
28+
from .status import ERROR_INVALID_PARAMS, ERROR_METHOD_NOT_FOUND, ERROR_INTERNAL_ERROR
2929

3030
# This is used to indicate when a value isn't present. We use this instead of
3131
# None, because None is a valid JSON-serializable type.
@@ -47,12 +47,12 @@ class Error(NamedTuple):
4747

4848

4949
def InvalidParams(data: Any = UNSPECIFIED) -> Error:
50-
return Error(status.JSONRPC_INVALID_PARAMS_CODE, "Invalid params", data)
50+
return Error(ERROR_INVALID_PARAMS, "Invalid params", data)
5151

5252

5353
def MethodNotFound(data: Any) -> Error:
54-
return Error(status.JSONRPC_METHOD_NOT_FOUND_CODE, "Method not found", data)
54+
return Error(ERROR_METHOD_NOT_FOUND, "Method not found", data)
5555

5656

5757
def InternalError(data: Any) -> Error:
58-
return Error(status.JSONRPC_INTERNAL_ERROR_CODE, "Internal error", data)
58+
return Error(ERROR_INTERNAL_ERROR, "Internal error", data)

jsonrpcserver/status.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@
6767
HTTP_INTERNAL_ERROR = 500
6868

6969
# JSONRPC status codes from http://www.jsonrpc.org/specification#error_object
70-
JSONRPC_PARSE_ERROR_CODE = -32700
71-
JSONRPC_INVALID_REQUEST_CODE = -32600
72-
JSONRPC_METHOD_NOT_FOUND_CODE = -32601
73-
JSONRPC_INVALID_PARAMS_CODE = -32602
74-
JSONRPC_INTERNAL_ERROR_CODE = -32603
75-
JSONRPC_SERVER_ERROR_CODE = -32000
70+
ERROR_PARSE_ERROR = -32700
71+
ERROR_INVALID_REQUEST = -32600
72+
ERROR_METHOD_NOT_FOUND = -32601
73+
ERROR_INVALID_PARAMS = -32602
74+
ERROR_INTERNAL_ERROR = -32603
75+
ERROR_SERVER_ERROR = -32000

tests/test_dispatcher.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from typing import Any
44
from unittest.mock import sentinel
55

6-
from jsonrpcserver import status
76
from jsonrpcserver.dispatcher import (
87
create_requests,
98
default_deserializer,
@@ -13,9 +12,15 @@
1312
dispatch_to_response_pure,
1413
)
1514
from jsonrpcserver.methods import Methods, global_methods
16-
from jsonrpcserver.result import Result, Success
1715
from jsonrpcserver.request import Request, NOID
1816
from jsonrpcserver.response import ErrorResponse, SuccessResponse
17+
from jsonrpcserver.result import Result, Success, InvalidParams
18+
from jsonrpcserver.status import (
19+
ERROR_INTERNAL_ERROR,
20+
ERROR_INVALID_PARAMS,
21+
ERROR_INVALID_REQUEST,
22+
ERROR_PARSE_ERROR,
23+
)
1924

2025

2126
# def test_dispatch_to_response_pure_invalid_params_notification():
@@ -139,27 +144,29 @@ def test_dispatch_to_response_pure_notification():
139144
assert response is None
140145

141146

142-
def test_dispatch_to_response_pure_notification_invalid_jsonrpc():
147+
def test_dispatch_to_response_pure_invalid_json():
148+
"""Unable to parse, must return an error"""
143149
response = dispatch_to_response_pure(
144150
methods=Methods(ping),
145151
context=None,
146152
schema_validator=default_schema_validator,
147153
deserializer=default_deserializer,
148-
request='{"jsonrpc": "0", "method": "notify"}',
154+
request="{",
149155
)
150156
assert isinstance(response, ErrorResponse)
157+
assert response.code == ERROR_PARSE_ERROR
151158

152159

153-
def test_dispatch_to_response_pure_invalid_json():
154-
"""Unable to parse, must return an error"""
160+
def test_dispatch_to_response_pure_notification_invalid_jsonrpc():
155161
response = dispatch_to_response_pure(
156162
methods=Methods(ping),
157163
context=None,
158164
schema_validator=default_schema_validator,
159165
deserializer=default_deserializer,
160-
request="{",
166+
request='{"jsonrpc": "0", "method": "notify"}',
161167
)
162168
assert isinstance(response, ErrorResponse)
169+
assert response.code == ERROR_INVALID_REQUEST
163170

164171

165172
def test_dispatch_to_response_pure_invalid_jsonrpc():
@@ -172,6 +179,7 @@ def test_dispatch_to_response_pure_invalid_jsonrpc():
172179
request="{}",
173180
)
174181
assert isinstance(response, ErrorResponse)
182+
assert response.code == ERROR_INVALID_REQUEST
175183

176184

177185
def test_dispatch_to_response_pure_invalid_params():
@@ -187,6 +195,7 @@ def foo(colour: str) -> Result:
187195
request='{"jsonrpc": "2.0", "method": "foo", "params": ["blue"], "id": 1}',
188196
)
189197
assert isinstance(response, ErrorResponse)
198+
assert response.code == ERROR_INVALID_PARAMS
190199

191200

192201
def test_dispatch_to_response_pure_invalid_params_count():
@@ -201,7 +210,25 @@ def foo(colour: str, size: str):
201210
request='{"jsonrpc": "2.0", "method": "foo", "params": {"colour":"blue"}, "id": 1}',
202211
)
203212
assert isinstance(response, ErrorResponse)
204-
assert response.data == "missing a required argument: 'size'"
213+
assert response.code == ERROR_INVALID_PARAMS
214+
215+
216+
def test_dispatch_to_response_pure_enforcing_result():
217+
"""Methods should return a Result, otherwise we get an Internal Error response."""
218+
219+
def not_a_result():
220+
return None
221+
222+
response = dispatch_to_response_pure(
223+
methods=Methods(not_a_result),
224+
context=None,
225+
schema_validator=default_schema_validator,
226+
deserializer=default_deserializer,
227+
request='{"jsonrpc": "2.0", "method": "not_a_result", "id": 1}',
228+
)
229+
assert isinstance(response, ErrorResponse)
230+
assert response.code == ERROR_INTERNAL_ERROR
231+
assert response.data == "The method did not return a Result"
205232

206233

207234
# dispatch_to_response
@@ -306,7 +333,7 @@ def test_examples_invalid_json():
306333
request='[{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method"]',
307334
)
308335
assert isinstance(response, ErrorResponse)
309-
assert response.code == status.JSONRPC_PARSE_ERROR_CODE
336+
assert response.code == ERROR_PARSE_ERROR
310337

311338

312339
def test_examples_empty_array():
@@ -319,7 +346,7 @@ def test_examples_empty_array():
319346
deserializer=default_deserializer,
320347
)
321348
assert isinstance(response, ErrorResponse)
322-
assert response.code == status.JSONRPC_INVALID_REQUEST_CODE
349+
assert response.code == ERROR_INVALID_REQUEST
323350

324351

325352
def test_examples_invalid_jsonrpc_batch():
@@ -335,7 +362,7 @@ def test_examples_invalid_jsonrpc_batch():
335362
request="[1]",
336363
)
337364
assert isinstance(response, ErrorResponse)
338-
assert response.code == status.JSONRPC_INVALID_REQUEST_CODE
365+
assert response.code == ERROR_INVALID_REQUEST
339366

340367

341368
def test_examples_multiple_invalid_jsonrpc():
@@ -351,7 +378,7 @@ def test_examples_multiple_invalid_jsonrpc():
351378
request="[1, 2, 3]",
352379
)
353380
assert isinstance(response, ErrorResponse)
354-
assert response.code == status.JSONRPC_INVALID_REQUEST_CODE
381+
assert response.code == ERROR_INVALID_REQUEST
355382

356383

357384
def test_examples_mixed_requests_and_notifications():

0 commit comments

Comments
 (0)