Skip to content

http.server and serve()

http.server

Python's built-in HTTP server, with no other dependencies. It reads the body itself, so it also has to check the Content-Length header, refuse a body that's too big and handle bytes that aren't UTF-8:

import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from jsonrpcserver import Result, Success, dispatch, method

MAX_BODY = 1_000_000  # bytes


@method
def ping() -> Result:
    return Success("pong")


class Handler(BaseHTTPRequestHandler):
    def do_POST(self) -> None:
        length = self.headers.get("Content-Length", "")
        if not length.isdecimal():
            self.send_error(411, "Content-Length required")
            return
        if int(length) > MAX_BODY:
            self.send_error(413, "Request body too large")
            return
        try:
            request = self.rfile.read(int(length)).decode("utf-8")
        except UnicodeDecodeError:
            response = json.dumps(
                {
                    "jsonrpc": "2.0",
                    "error": {"code": -32700, "message": "Parse error"},
                    "id": None,
                }
            )
        else:
            # max_batch_size: see the Security page.
            response = dispatch(request, max_batch_size=100)
        if response:
            body = response.encode()
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            # A notification. There's nothing to send back.
            self.send_response(204)
            self.end_headers()


if __name__ == "__main__":
    ThreadingHTTPServer(("localhost", 8000), Handler).serve_forever()

A missing Content-Length gets 411, a body over the limit gets 413, and a body that isn't UTF-8 gets a -32700 "Parse error". http.server is fine for small internal tools. For anything public, use a framework or put a production web server in front.

serve()

jsonrpcserver also has a small built-in server, serve(). It's for trying things out, not for production:

from jsonrpcserver import Result, Success, method, serve


@method
def ping() -> Result:
    return Success("pong")


if __name__ == "__main__":
    serve("localhost", 8000)

It answers POST requests on any path, sends 204 for notifications, and handles each request in its own thread. It also handles a missing Content-Length (411) and a body that isn't UTF-8 (-32700).

Know its limits before you use it:

  • It listens on every network interface unless you pass a host, as the example does with "localhost". With serve() and no arguments, anyone who can reach your machine on port 5000 can call your methods.
  • It has no TLS, no authentication and no limit on the body size, and it doesn't set max_batch_size.
  • It says where it's listening when it starts (new in 5.0.10). Each request is logged on the jsonrpcserver.server logger at INFO level, so logging.basicConfig(level=logging.INFO) shows them.

Changed in 5.0.10

In 5.0.9, serve() answers a notification with 200 and an empty body instead of 204. It also handles one request at a time. It logs its start on the root logger at INFO, so you usually see nothing. It drops the connection for a missing Content-Length or a body that isn't UTF-8.

Try it

Save the serve() example as quickstart.py and run it:

python quickstart.py

Then send it a request from another terminal:

curl -s -H 'Content-Type: application/json' -d '{"jsonrpc": "2.0", "method": "ping", "id": 1}' http://localhost:8000/
Output
{"jsonrpc": "2.0", "result": "pong", "id": 1}

The jsonrpcclient quickstart sends the same request from Python.