Skip to content

Context

Methods often need something from the server that the client mustn't control: the logged-in user, the HTTP request, a database session. Pass it as context. It becomes the first argument of every method, and the client can't see or change it.

A small class keeps the context tidy when there's more than one thing to pass:

from dataclasses import dataclass
from typing import Any

from jsonrpcserver import Result, Success, dispatch


@dataclass
class Context:
    user: str
    request: Any


def whoami(context: Context) -> Result:
    return Success(context.user)


def user_for(authorization: str) -> str:
    # Look the token up in your user store. This is a stand-in.
    return {"Bearer abc123": "beau"}.get(authorization, "anonymous")


METHODS = {"whoami": whoami}

The method's other parameters come after context, and the request's params fill those as usual.

Flask

from flask import Flask, Response, request

app = Flask(__name__)


@app.post("/")
def index() -> Response:
    context = Context(user_for(request.headers.get("Authorization", "")), request)
    if response := dispatch(
        request.get_data(as_text=True), METHODS, context=context, max_batch_size=100
    ):
        return Response(response, content_type="application/json")
    return Response(status=204)

Trying it with Flask's test client:

>>> client = app.test_client()
>>> client.post(
...     "/",
...     data='{"jsonrpc": "2.0", "method": "whoami", "id": 1}',
...     headers={"Authorization": "Bearer abc123"},
... ).get_data(as_text=True)
'{"jsonrpc": "2.0", "result": "beau", "id": 1}'

FastAPI

from fastapi import FastAPI, Request
from fastapi import Response as FastAPIResponse

from jsonrpcserver import async_dispatch

api = FastAPI()


@api.post("/")
async def endpoint(request: Request) -> FastAPIResponse:
    context = Context(user_for(request.headers.get("Authorization", "")), request)
    body = (await request.body()).decode()
    if response := await async_dispatch(
        body, METHODS, context=context, max_batch_size=100
    ):
        return FastAPIResponse(response, media_type="application/json")
    return FastAPIResponse(status_code=204)

async_dispatch calls the plain whoami method too. To try it, FastAPI's test client needs httpx2:

>>> from fastapi.testclient import TestClient
>>> TestClient(api).post(
...     "/",
...     content='{"jsonrpc": "2.0", "method": "whoami", "id": 1}',
...     headers={"Authorization": "Bearer abc123"},
... ).text
'{"jsonrpc": "2.0", "result": "beau", "id": 1}'

FastAPI's Depends doesn't reach into methods, because jsonrpcserver calls them, not FastAPI. Resolve what you need in the endpoint, as above, and pass it in the context.

Django

from django.http import HttpRequest, HttpResponse
from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def jsonrpc(request: HttpRequest) -> HttpResponse:
    # With Django's authentication, use request.user instead of user_for.
    context = Context(user_for(request.headers.get("Authorization", "")), request)
    if response := dispatch(
        request.body.decode(), METHODS, context=context, max_batch_size=100
    ):
        return HttpResponse(response, content_type="application/json")
    return HttpResponse(status=204)

A database session per request

Open the session in the view, pass it in the context, and close it when dispatch returns. Every method in a batch then shares the one session:

from contextlib import closing
import sqlite3


def count_users(context: sqlite3.Connection) -> Result:
    (count,) = context.execute("SELECT count(*) FROM users").fetchone()
    return Success(count)


def handle(request_body: str) -> str:
    with closing(sqlite3.connect(":memory:")) as db:
        db.execute("CREATE TABLE users (name TEXT)")  # Stand-in for a real database.
        return dispatch(
            request_body, {"count_users": count_users}, context=db, max_batch_size=100
        )
>>> handle('{"jsonrpc": "2.0", "method": "count_users", "id": 1}')
'{"jsonrpc": "2.0", "result": 0, "id": 1}'

With async_dispatch, the requests in a batch run concurrently. Make sure the shared object can be used that way, or open what you need in each method.