Validation¶
After parsing a request, jsonrpcserver checks it against the JSON-RPC spec. A request that fails gets a -32600 "Invalid request" response, and no method runs.
What the default checks¶
The default validator checks each request against a JSON schema. A request must be an object with:
"jsonrpc": "2.0", exactly- a
methodthat's a string params, if present, that's an array or an object- an
id, if present, that's a string, a number or null
Nothing else is allowed in the object.
from jsonrpcserver import Result, Success, dispatch, method
@method
def ping() -> Result:
return Success("pong")
>>> dispatch('{"jsonrpc": "2.0", "method": "ping", "params": "x", "id": 1}')
'{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid request", "data": "The request failed schema validation"}, "id": null}'
The error doesn't say which rule the request broke, and its id is null
because the request couldn't be trusted.
A custom validator¶
Pass validator to use your own. It gets the parsed request, usually a
dict, and should raise an exception of any kind if the request is invalid.
It can also get anything else that parsed, such as a number, a string or an
empty list, so don't assume a dict. What it
returns is ignored. In a batch, it's called once for each request.
This one runs the default checks, then refuses requests without an id, so
clients can't send notifications:
from typing import Any, Dict
from jsonrpcserver.main import default_validator
def no_notifications(request: Dict[str, Any]) -> None:
default_validator(request)
if "id" not in request:
raise ValueError("Notifications aren't allowed")
>>> dispatch('{"jsonrpc": "2.0", "method": "ping"}', validator=no_notifications)
'{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid request", "data": "The request failed schema validation"}, "id": null}'
The exception's message isn't sent to the client.
Changed in 5.0.10
Before 5.0.10, a batch was validated as a whole: the validator was called
once, with the list. A validator that enforced a rule about the whole
batch, such as refusing batches, no longer sees the list. Use
max_batch_size for a size limit.
Turning it off¶
Validation takes most of the time dispatch spends on a small method. If
your own code makes the requests, so you know they're valid, you can turn it
off:
>>> dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}', validator=lambda _: None)
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
Without it, dispatch still never raises, but bad requests get odd answers:
>>> import logging
>>> logging.disable(logging.CRITICAL) # Keep the logged traceback out of this page.
>>> no_validation = lambda _: None
>>> dispatch('{"method": "ping", "id": 1}', validator=no_validation)
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
>>> dispatch(
... '{"jsonrpc": "2.0", "method": "ping", "params": "x", "id": 1}',
... validator=no_validation,
... )
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
>>> dispatch('{"jsonrpc": "2.0", "id": 1}', validator=no_validation)
'{"jsonrpc": "2.0", "error": {"code": -32000, "message": "Server error"}, "id": null}'
>>> logging.disable(logging.NOTSET)
A request with no jsonrpc member runs, and params that aren't a list or an
object are ignored. A request with no method gets a -32000 "Server error",
and jsonrpcserver logs it as an error of its own. An empty batch, [], gets
no response at all instead of an error. Keep validation on for anything that
strangers can reach.
NaN, Infinity and huge numbers¶
Python's json module accepts NaN, Infinity and numbers like 1e400 in
a request. They aren't valid JSON, and the schema can't see them, because
they're already floats by the time it runs. To reject them, pass a stricter
deserializer:
import json
import math
def reject(constant: str) -> None:
raise ValueError(f"{constant} is not valid JSON")
def finite_float(text: str) -> float:
number = float(text)
if math.isinf(number):
raise ValueError(f"{text} is too big")
return number
def strict_loads(request: str) -> Any:
return json.loads(request, parse_constant=reject, parse_float=finite_float)
>>> dispatch(
... '{"jsonrpc": "2.0", "method": "ping", "params": [NaN], "id": 1}',
... deserializer=strict_loads,
... )
'{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error", "data": "NaN is not valid JSON"}, "id": null}'
In 5.0.10, dispatch and async_dispatch never write them with the default
serializer. It refuses, and the client gets an Internal error instead. 5.0.9
writes them as they are. dispatch_to_serializable returns the float itself,
so if your framework serializes the dict, check how it treats them.