Skip to content

Commit 96e72a4

Browse files
authored
Fix schema validation (#175)
* Fix schema validation Stop using the global schema in pure functions. Also now properly allows custom schema validators. Next step for fixing schema validation is #166 * Remove unused "validate" function
1 parent cb89a45 commit 96e72a4

File tree

2 files changed

+77
-49
lines changed

2 files changed

+77
-49
lines changed

jsonrpcserver/dispatcher.py

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from typing import Any, Callable, Dict, List, Union
1212

1313
from apply_defaults import apply_config # type: ignore
14-
from jsonschema import ValidationError # type: ignore
1514
from jsonschema.validators import validator_for # type: ignore
1615
from pkg_resources import resource_string # type: ignore
1716

@@ -28,11 +27,14 @@
2827
)
2928
from .result import InvalidParams, InternalError, Result
3029

31-
# Prepare the jsonschema validator
32-
global_schema = json.loads(resource_string(__name__, "request-schema.json"))
33-
klass = validator_for(global_schema)
34-
klass.check_schema(global_schema)
35-
validator = klass(global_schema)
30+
default_deserializer = json.loads
31+
32+
# Prepare the jsonschema validator. This is global so it loads only once, not every
33+
# time dispatch is called.
34+
schema = json.loads(resource_string(__name__, "request-schema.json"))
35+
klass = validator_for(schema)
36+
klass.check_schema(schema)
37+
default_schema_validator = klass(schema).validate
3638

3739
# Read configuration file
3840
config = ConfigParser(default_section="dispatch")
@@ -140,27 +142,13 @@ def create_requests(requests: Union[Dict, List[Dict]]) -> Union[Request, List[Re
140142
)
141143

142144

143-
def validate(request: Union[Dict, List]) -> Union[Dict, List]:
144-
"""
145-
Wraps jsonschema.validate, returning the same object passed in if successful.
146-
147-
Raises an exception if invalid.
148-
149-
Args:
150-
request: The deserialized-from-json request.
151-
152-
Returns:
153-
The same object passed in.
154-
155-
Raises:
156-
jsonschema.ValidationError
157-
"""
158-
validator.validate(request)
159-
return request
160-
161-
162145
def dispatch_to_response_pure(
163-
*, methods: Methods, context: Any, deserializer: Callable, request: str
146+
*,
147+
methods: Methods,
148+
context: Any,
149+
schema_validator: Callable,
150+
deserializer: Callable,
151+
request: str,
164152
) -> Union[Response, List[Response], None]:
165153
"""
166154
Dispatch a JSON-serialized request string to methods.
@@ -186,9 +174,12 @@ def dispatch_to_response_pure(
186174
# will be raised is unknown. Any exception is a parse error.
187175
except Exception as exc:
188176
return ParseErrorResponse(str(exc))
177+
# As above, we don't know which validator will be used, so the specific
178+
# exception that will be raised is unknown. Any exception is an invalid request
179+
# error.
189180
try:
190-
validate(deserialized)
191-
except ValidationError as exc:
181+
schema_validator(deserialized)
182+
except Exception as exc:
192183
return InvalidRequestResponse("The request failed schema validation")
193184
return dispatch_requests(
194185
methods=methods, context=context, requests=create_requests(deserialized)
@@ -204,7 +195,8 @@ def dispatch_to_response(
204195
methods: Methods = None,
205196
*,
206197
context: Any = None,
207-
deserializer: Callable = json.loads,
198+
schema_validator: Callable = default_schema_validator,
199+
deserializer: Callable = default_deserializer,
208200
) -> Union[Response, List[Response], None]:
209201
"""
210202
Dispatch a JSON-serialized request to methods.
@@ -218,9 +210,10 @@ def dispatch_to_response(
218210
request: The JSON-RPC request string.
219211
methods: Collection of methods that can be called. If not passed, uses the
220212
internal methods object.
221-
request: The incoming request string.
222213
context: Will be passed to methods as the first param if not None.
214+
schema_validator:
223215
deserialize: Function that is used to deserialize data.
216+
request: The incoming request string.
224217
225218
Returns:
226219
A Response, list of Responses or None.
@@ -231,13 +224,16 @@ def dispatch_to_response(
231224
return dispatch_to_response_pure(
232225
methods=global_methods if methods is None else methods,
233226
context=context,
227+
schema_validator=schema_validator,
234228
deserializer=deserializer,
235229
request=request,
236230
)
237231

238232

239233
def dispatch_to_json(
240-
*args: Any, serializer: Callable = json.dumps, **kwargs: Any
234+
*args: Any,
235+
serializer: Callable = json.dumps,
236+
**kwargs: Any,
241237
) -> str:
242238
"""
243239
This is the main public method, it goes through the entire JSON-RPC process

tests/test_dispatcher.py

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from jsonrpcserver import status
77
from jsonrpcserver.dispatcher import (
88
create_requests,
9+
default_deserializer,
10+
default_schema_validator,
911
dispatch_request,
1012
dispatch_to_response,
1113
dispatch_to_response_pure,
@@ -117,7 +119,8 @@ def test_dispatch_to_response_pure():
117119
response = dispatch_to_response_pure(
118120
methods=Methods(ping),
119121
context=None,
120-
deserializer=json.loads,
122+
schema_validator=default_schema_validator,
123+
deserializer=default_deserializer,
121124
request='{"jsonrpc": "2.0", "method": "ping", "id": 1}',
122125
)
123126
assert isinstance(response, SuccessResponse)
@@ -129,7 +132,8 @@ def test_dispatch_to_response_pure_notification():
129132
response = dispatch_to_response_pure(
130133
methods=Methods(ping),
131134
context=None,
132-
deserializer=json.loads,
135+
schema_validator=default_schema_validator,
136+
deserializer=default_deserializer,
133137
request='{"jsonrpc": "2.0", "method": "ping"}',
134138
)
135139
assert response is None
@@ -139,7 +143,8 @@ def test_dispatch_to_response_pure_notification_invalid_jsonrpc():
139143
response = dispatch_to_response_pure(
140144
methods=Methods(ping),
141145
context=None,
142-
deserializer=json.loads,
146+
schema_validator=default_schema_validator,
147+
deserializer=default_deserializer,
143148
request='{"jsonrpc": "0", "method": "notify"}',
144149
)
145150
assert isinstance(response, ErrorResponse)
@@ -148,15 +153,23 @@ def test_dispatch_to_response_pure_notification_invalid_jsonrpc():
148153
def test_dispatch_to_response_pure_invalid_json():
149154
"""Unable to parse, must return an error"""
150155
response = dispatch_to_response_pure(
151-
methods=Methods(ping), context=None, deserializer=json.loads, request="{"
156+
methods=Methods(ping),
157+
context=None,
158+
schema_validator=default_schema_validator,
159+
deserializer=default_deserializer,
160+
request="{",
152161
)
153162
assert isinstance(response, ErrorResponse)
154163

155164

156165
def test_dispatch_to_response_pure_invalid_jsonrpc():
157166
"""Invalid JSON-RPC, must return an error. (impossible to determine if notification)"""
158167
response = dispatch_to_response_pure(
159-
methods=Methods(ping), context=None, deserializer=json.loads, request="{}"
168+
methods=Methods(ping),
169+
context=None,
170+
schema_validator=default_schema_validator,
171+
deserializer=default_deserializer,
172+
request="{}",
160173
)
161174
assert isinstance(response, ErrorResponse)
162175

@@ -169,7 +182,8 @@ def foo(colour: str) -> Result:
169182
response = dispatch_to_response_pure(
170183
methods=Methods(foo),
171184
context=None,
172-
deserializer=json.loads,
185+
schema_validator=default_schema_validator,
186+
deserializer=default_deserializer,
173187
request='{"jsonrpc": "2.0", "method": "foo", "params": ["blue"], "id": 1}',
174188
)
175189
assert isinstance(response, ErrorResponse)
@@ -182,7 +196,8 @@ def foo(colour: str, size: str):
182196
response = dispatch_to_response_pure(
183197
methods=Methods(foo),
184198
context=None,
185-
deserializer=json.loads,
199+
schema_validator=default_schema_validator,
200+
deserializer=default_deserializer,
186201
request='{"jsonrpc": "2.0", "method": "foo", "params": {"colour":"blue"}, "id": 1}',
187202
)
188203
assert isinstance(response, ErrorResponse)
@@ -216,7 +231,8 @@ def subtract(minuend, subtrahend):
216231
response = dispatch_to_response_pure(
217232
methods=Methods(subtract),
218233
context=None,
219-
deserializer=json.loads,
234+
schema_validator=default_schema_validator,
235+
deserializer=default_deserializer,
220236
request='{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}',
221237
)
222238
assert isinstance(response, SuccessResponse)
@@ -226,7 +242,8 @@ def subtract(minuend, subtrahend):
226242
response = dispatch_to_response_pure(
227243
methods=Methods(subtract),
228244
context=None,
229-
deserializer=json.loads,
245+
schema_validator=default_schema_validator,
246+
deserializer=default_deserializer,
230247
request='{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}',
231248
)
232249
assert isinstance(response, SuccessResponse)
@@ -240,7 +257,8 @@ def subtract(**kwargs):
240257
response = dispatch_to_response_pure(
241258
methods=Methods(subtract),
242259
context=None,
243-
deserializer=json.loads,
260+
schema_validator=default_schema_validator,
261+
deserializer=default_deserializer,
244262
request='{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}',
245263
)
246264
assert isinstance(response, SuccessResponse)
@@ -250,7 +268,8 @@ def subtract(**kwargs):
250268
response = dispatch_to_response_pure(
251269
methods=Methods(subtract),
252270
context=None,
253-
deserializer=json.loads,
271+
schema_validator=default_schema_validator,
272+
deserializer=default_deserializer,
254273
request='{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}',
255274
)
256275
assert isinstance(response, SuccessResponse)
@@ -261,7 +280,8 @@ def test_examples_notification():
261280
response = dispatch_to_response_pure(
262281
methods=Methods(update=lambda: None, foobar=lambda: None),
263282
context=None,
264-
deserializer=json.loads,
283+
schema_validator=default_schema_validator,
284+
deserializer=default_deserializer,
265285
request='{"jsonrpc": "2.0", "method": "update", "params": [1, 2, 3, 4, 5]}',
266286
)
267287
assert response is None
@@ -270,7 +290,8 @@ def test_examples_notification():
270290
response = dispatch_to_response_pure(
271291
methods=Methods(update=lambda: None, foobar=lambda: None),
272292
context=None,
273-
deserializer=json.loads,
293+
schema_validator=default_schema_validator,
294+
deserializer=default_deserializer,
274295
request='{"jsonrpc": "2.0", "method": "foobar"}',
275296
)
276297
assert response is None
@@ -280,7 +301,8 @@ def test_examples_invalid_json():
280301
response = dispatch_to_response_pure(
281302
methods=Methods(ping),
282303
context=None,
283-
deserializer=json.loads,
304+
schema_validator=default_schema_validator,
305+
deserializer=default_deserializer,
284306
request='[{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method"]',
285307
)
286308
assert isinstance(response, ErrorResponse)
@@ -293,7 +315,8 @@ def test_examples_empty_array():
293315
request="[]",
294316
methods=Methods(ping),
295317
context=None,
296-
deserializer=json.loads,
318+
schema_validator=default_schema_validator,
319+
deserializer=default_deserializer,
297320
)
298321
assert isinstance(response, ErrorResponse)
299322
assert response.code == status.JSONRPC_INVALID_REQUEST_CODE
@@ -305,7 +328,11 @@ def test_examples_invalid_jsonrpc_batch():
305328
The examples are expecting a batch response full of error responses.
306329
"""
307330
response = dispatch_to_response_pure(
308-
methods=Methods(ping), context=None, deserializer=json.loads, request="[1]"
331+
methods=Methods(ping),
332+
context=None,
333+
schema_validator=default_schema_validator,
334+
deserializer=default_deserializer,
335+
request="[1]",
309336
)
310337
assert isinstance(response, ErrorResponse)
311338
assert response.code == status.JSONRPC_INVALID_REQUEST_CODE
@@ -319,7 +346,8 @@ def test_examples_multiple_invalid_jsonrpc():
319346
response = dispatch_to_response_pure(
320347
methods=Methods(ping),
321348
context=None,
322-
deserializer=json.loads,
349+
schema_validator=default_schema_validator,
350+
deserializer=default_deserializer,
323351
request="[1, 2, 3]",
324352
)
325353
assert isinstance(response, ErrorResponse)
@@ -357,7 +385,11 @@ def test_examples_mixed_requests_and_notifications():
357385
]
358386
)
359387
response = dispatch_to_response_pure(
360-
methods=methods, context=None, deserializer=json.loads, request=requests
388+
methods=methods,
389+
context=None,
390+
schema_validator=default_schema_validator,
391+
deserializer=default_deserializer,
392+
request=requests,
361393
)
362394
expected = [
363395
SuccessResponse(

0 commit comments

Comments
 (0)