Dispatch¶
dispatch takes a JSON-RPC request string, calls the method and gives a
JSON-RPC response string.
from jsonrpcserver import Result, Success, dispatch, method
@method
def ping() -> Result:
return Success("pong")
>>> dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}')
'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
It never raises for a bad request or a failing method. Those become JSON-RPC error responses, as the spec requires:
>>> dispatch('{"jsonrpc": "2.0", "method": "nope", "id": 1}')
'{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found", "data": "nope"}, "id": 1}'
>>> dispatch("{")
'{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error", "data": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, "id": null}'
Errors and logging lists every error it can send.
A notification, or a batch of only notifications, gives an empty string. The spec says not to respond to those. Notifications and batches covers both.
See how dispatch is used in different frameworks.
Options¶
All of these are keyword arguments, apart from methods, which can also be
the second positional argument.
methods¶
The methods that requests can call, as a dict of names to functions. Use this
instead of the @method decorator:
def multiply(a: int, b: int) -> Result:
return Success(a * b)
>>> dispatch(
... '{"jsonrpc": "2.0", "method": "multiply", "params": [2, 3], "id": 1}',
... methods={"multiply": multiply},
... )
'{"jsonrpc": "2.0", "result": 6, "id": 1}'
The default is the dict that @method fills in,
jsonrpcserver.methods.global_methods. Any mapping works, not only a dict.
context¶
If given, it's passed as the first argument to every method. Use it for things like the database connection or the logged-in user:
def greet(context: str, name: str) -> Result:
return Success(context + " " + name)
>>> dispatch(
... '{"jsonrpc": "2.0", "method": "greet", "params": ["Beau"], "id": 1}',
... methods={"greet": greet},
... context="Hello",
... )
'{"jsonrpc": "2.0", "result": "Hello Beau", "id": 1}'
The client can't see or set it. Context shows how to pass the HTTP request or the user from Flask, FastAPI and Django.
debug¶
When a method raises an exception it doesn't catch, the client gets a -32603
"Internal error" with no details, and the exception is logged. With
debug=True, the exception message goes into the response's data too:
def broken() -> Result:
raise ValueError("Something went wrong")
>>> import logging
>>> logging.disable(logging.CRITICAL) # Keep the logged traceback out of this page.
>>> dispatch('{"jsonrpc": "2.0", "method": "broken", "id": 1}', methods={"broken": broken})
'{"jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal error"}, "id": 1}'
>>> dispatch(
... '{"jsonrpc": "2.0", "method": "broken", "id": 1}',
... methods={"broken": broken},
... debug=True,
... )
'{"jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal error", "data": "Something went wrong"}, "id": 1}'
>>> logging.disable(logging.NOTSET)
Only use it in development. Exception messages can include passwords, file
paths and SQL. The default is False.
New in 5.0.10
The debug option, and leaving the message out by default. In 5.0.0 to
5.0.9 the message is always sent, and debug raises TypeError. (4.x
had debug too, off by default.) See
Security.
max_batch_size¶
The most requests a batch may hold. A bigger batch gets a single -32600 "Invalid request" response, and none of its requests are run:
>>> dispatch(
... '[{"jsonrpc": "2.0", "method": "ping", "id": 1}, {"jsonrpc": "2.0", "method": "ping", "id": 2}]',
... max_batch_size=1,
... )
'{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid request", "data": "The batch has 2 requests. The limit is 1."}, "id": null}'
The default, None, means no limit. A server open to the internet should set
one. See Security. Anything other than None
or a positive int raises ValueError.
New in 5.0.10
5.0.9 raises TypeError for this keyword.
deserializer¶
The function that parses the request string. The default is json.loads.
import ujson
dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}', deserializer=ujson.loads)
If it raises, the client gets a -32700 "Parse error" whose data is the
exception's message, so don't put anything secret in it.
serializer¶
The function that turns the response into a string. The default is
json.dumps with allow_nan=False, so a result containing NaN or
Infinity gives an Internal error instead of output that isn't valid JSON.
If the serializer raises for a response (say the method returned a
datetime), that response becomes an Internal error, and the error is logged.
The rest of a batch is sent as usual.
dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}', serializer=ujson.dumps)
Changed in 5.0.10
5.0.9 writes NaN and Infinity into the response, and raises when the
serializer fails.
validator¶
The function that checks each request against the JSON-RPC spec, after parsing. The default checks against a JSON schema. Validation says what it checks, how to write your own, and what you lose by turning it off.
Changed in 5.0.10
In a batch, the validator is called once for each request. In 5.0.9 it was called once with the whole list.
Other return types¶
dispatch is also called dispatch_to_json. Two other functions take the
same options, apart from serializer, and give the response in other forms.
dispatch_to_serializable¶
It gives a dict, a list of dicts for a batch, or None for a notification.
Use it when your framework serializes the response itself:
>>> from jsonrpcserver import dispatch_to_serializable
>>> dispatch_to_serializable('{"jsonrpc": "2.0", "method": "ping", "id": 1}')
{'jsonrpc': '2.0', 'result': 'pong', 'id': 1}
dispatch_to_response¶
It gives Response objects, or None for a notification. Use it to look at
or change responses before they're serialized. It also takes a post_process
function, which is applied to each response.
A Response comes from the oslash
library. It's a Right holding a SuccessResponse, or a Left holding an
ErrorResponse. oslash has no public way to read them, so check which one you
have and read _value or _error. Those attributes are stable for all of
5.x:
>>> from oslash.either import Left
>>> from jsonrpcserver import dispatch_to_response
>>> response = dispatch_to_response('{"jsonrpc": "2.0", "method": "ping", "id": 1}')
>>> isinstance(response, Left)
False
>>> response._value
SuccessResponse(result='pong', id=1)
>>> error = dispatch_to_response('{"jsonrpc": "2.0", "method": "nope", "id": 1}')
>>> isinstance(error, Left)
True
>>> error._error
ErrorResponse(code=-32601, message='Method not found', data='nope', id=1)
print(response) raises TypeError: not all arguments converted during
string formatting, because of a bug in oslash. Turn it into a dict first with
to_dict:
>>> from jsonrpcserver.response import to_dict
>>> print(to_dict(response))
{'jsonrpc': '2.0', 'result': 'pong', 'id': 1}
For a batch, it gives a list of Responses. Everything here works the same
with async_dispatch_to_serializable and async_dispatch_to_response. See
Async.