Skip to content

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 the JSON-RPC 2.0 rules. 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 is a SuccessResponse or an ErrorResponse. Both are frozen dataclasses, so read their fields directly:

>>> from jsonrpcserver import dispatch_to_response
>>> from jsonrpcserver.response import ErrorResponse
>>> response = dispatch_to_response('{"jsonrpc": "2.0", "method": "ping", "id": 1}')
>>> response
SuccessResponse(result='pong', id=1)
>>> response.result
'pong'
>>> error = dispatch_to_response('{"jsonrpc": "2.0", "method": "nope", "id": 1}')
>>> isinstance(error, ErrorResponse)
True
>>> error.code
-32601

to_dict turns either one into a JSON-RPC response dict:

>>> from jsonrpcserver.response import to_dict
>>> to_dict(response)
{'jsonrpc': '2.0', 'result': 'pong', 'id': 1}

Changed in 6.0

5.x wrapped responses in the oslash library's Right and Left, read through _value and _error. See Migration.

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.