From 6673b7a726be0fa364cf95f92a1d83e396fd967d Mon Sep 17 00:00:00 2001
From: bensynapse <118375461+bensynapse@users.noreply.github.com>
Date: Tue, 6 Oct 2026 05:59:55 +0300
Subject: [PATCH 1/2] Rewrite the docs and README, with an API reference and
tested examples
The docs site now has the same layout as jsonrpcclient's, in pink. It adds
one page per framework, an API reference built from the docstrings, and a
migration guide.
Every page is honest about the release that people actually get. A banner
says the docs describe 5.0.10 and PyPI has 5.0.9. Security opens with a
tested wrapper that stops 5.0.9 from sending exception messages to clients.
Each 5.0.10 feature is marked "New in" or "Changed in".
The examples follow the Security page. Each one sets max_batch_size and a
body size limit, and listens on localhost:8000, where the jsonrpcclient
examples connect. CI starts each one and sends it real requests. That includes a batch
over the limit and a body over the limit. The http.server example now handles
a missing Content-Length and bytes that aren't UTF-8. The quickstart starts a
server, calls it with curl and with jsonrpcclient, and both run in CI.
New pages cover notifications and batches, errors and logging (with every
logger), context, validation, typing, testing and threads. A new test calls
dispatch from many threads at once, and runs on free-threaded 3.14t.
The site gets per-page titles and descriptions, a social card, a neutral
favicon, a visible h1 and a 404 page. It also has the client's copy button
fix, accessibility fixes and an HTTPS redirect. The pink is darkened to
#c2185b so links and the header pass WCAG AA. A new CI job opens every page in
Chromium, light and dark, at desktop and phone widths. It fails on any serious
axe-core finding, a broken internal link or anchor, or a page that scrolls
sideways.
The README follows the agreed layout and renders cleanly on PyPI. The release
workflow refuses to publish while the docs still call the version unreleased.
RELEASING.md lists the clean-up steps and the advisory step.
---
.github/ISSUE_TEMPLATE/bug_report.yml | 3 +
.github/ISSUE_TEMPLATE/config.yml | 5 +-
.github/scripts/check_site.py | 194 +++++++++++++++++++++
.github/workflows/ci.yml | 33 +++-
.github/workflows/release.yml | 18 +-
CHANGELOG.md | 38 ++--
CONTRIBUTING.md | 17 +-
README.md | 109 +++++++++---
RELEASING.md | 32 +++-
SECURITY.md | 7 +-
docs/assets/a11y.js | 27 +++
docs/assets/apple-touch-icon.png | Bin 0 -> 7388 bytes
docs/assets/copy-without-prompts.js | 19 ++
docs/assets/extra.css | 191 ++++++++++----------
docs/assets/favicon.png | Bin 0 -> 2408 bytes
docs/assets/social-card.png | Bin 0 -> 67107 bytes
docs/async.md | 57 ++++--
docs/batches.md | 135 +++++++++++++++
docs/changelog.md | 5 +
docs/context.md | 163 ++++++++++++++++++
docs/contributing.md | 5 +
docs/dispatch.md | 148 +++++++++++-----
docs/errors.md | 181 +++++++++++++++++++
docs/examples.md | 158 ++++++-----------
docs/examples/aiohttp_server.py | 8 +-
docs/examples/check_examples.py | 145 +++++++++++++---
docs/examples/django_server.py | 9 +-
docs/examples/fastapi_server.py | 14 +-
docs/examples/flask_server.py | 7 +-
docs/examples/http_server.py | 27 ++-
docs/examples/{serve.py => quickstart.py} | 2 +-
docs/examples/sanic_server.py | 7 +-
docs/examples/socketio_server.py | 8 +-
docs/examples/tornado_server.py | 7 +-
docs/examples/websockets_server.py | 6 +-
docs/examples/werkzeug_server.py | 14 +-
docs/examples/zeromq_async_server.py | 7 +-
docs/examples/zeromq_server.py | 8 +-
docs/faq.md | 121 ++++++++++---
docs/frameworks/aiohttp.md | 38 ++++
docs/frameworks/django.md | 48 ++++++
docs/frameworks/fastapi.md | 44 +++++
docs/frameworks/flask.md | 41 +++++
docs/frameworks/http-server.md | 70 ++++++++
docs/frameworks/sanic.md | 40 +++++
docs/frameworks/socketio.md | 28 +++
docs/frameworks/tornado.md | 37 ++++
docs/frameworks/websockets.md | 38 ++++
docs/frameworks/werkzeug.md | 43 +++++
docs/frameworks/zeromq.md | 43 +++++
docs/index.md | 115 +++++++++----
docs/license.md | 12 ++
docs/methods.md | 78 ++++++---
docs/migration.md | 167 ++++++++++++++++++
docs/reference.md | 116 +++++++++++++
docs/roadmap.md | 25 ++-
docs/security-policy.md | 5 +
docs/security.md | 177 +++++++++++++------
docs/testing.md | 119 +++++++++++++
docs/threads.md | 63 +++++++
docs/typing.md | 76 ++++++++
docs/validation.md | 145 ++++++++++++++++
mkdocs.yml | 108 ++++++++++--
overrides/404.html | 22 +++
overrides/main.html | 49 ++++++
pyproject.toml | 15 +-
requirements/docs.txt | 2 +
requirements/examples.txt | 7 +
requirements/site.txt | 2 +
tests/doc_examples.py | 201 +++++++++++++++++-----
tests/test_docs.py | 121 ++++++++++++-
tests/test_threading.py | 92 ++++++++++
72 files changed, 3566 insertions(+), 556 deletions(-)
create mode 100644 .github/scripts/check_site.py
create mode 100644 docs/assets/a11y.js
create mode 100644 docs/assets/apple-touch-icon.png
create mode 100644 docs/assets/copy-without-prompts.js
create mode 100644 docs/assets/favicon.png
create mode 100644 docs/assets/social-card.png
create mode 100644 docs/batches.md
create mode 100644 docs/changelog.md
create mode 100644 docs/context.md
create mode 100644 docs/contributing.md
create mode 100644 docs/errors.md
rename docs/examples/{serve.py => quickstart.py} (83%)
create mode 100644 docs/frameworks/aiohttp.md
create mode 100644 docs/frameworks/django.md
create mode 100644 docs/frameworks/fastapi.md
create mode 100644 docs/frameworks/flask.md
create mode 100644 docs/frameworks/http-server.md
create mode 100644 docs/frameworks/sanic.md
create mode 100644 docs/frameworks/socketio.md
create mode 100644 docs/frameworks/tornado.md
create mode 100644 docs/frameworks/websockets.md
create mode 100644 docs/frameworks/werkzeug.md
create mode 100644 docs/frameworks/zeromq.md
create mode 100644 docs/license.md
create mode 100644 docs/migration.md
create mode 100644 docs/reference.md
create mode 100644 docs/security-policy.md
create mode 100644 docs/testing.md
create mode 100644 docs/threads.md
create mode 100644 docs/typing.md
create mode 100644 docs/validation.md
create mode 100644 overrides/404.html
create mode 100644 overrides/main.html
create mode 100644 requirements/site.txt
create mode 100644 tests/test_threading.py
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index b9df452..58b7fe8 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -13,6 +13,9 @@ body:
id: version
attributes:
label: jsonrpcserver version
+ description: >-
+ Run `pip show jsonrpcserver` to find it. From 5.0.10 you can also
+ print `jsonrpcserver.__version__`.
placeholder: "5.0.9"
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 719b40b..0484ccf 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,11 @@
blank_issues_enabled: true
contact_links:
+ - name: Ask a question
+ url: https://github.com/bensynapse/jsonrpcserver/discussions/categories/q-a
+ about: Questions about using jsonrpcserver are answered in Discussions.
- name: Report a security problem
url: https://github.com/bensynapse/jsonrpcserver/security/advisories/new
about: Please report security problems privately, not in a public issue.
- name: Documentation
url: https://bensynapse.github.io/jsonrpcserver/
- about: Usage and examples.
+ about: Guides, framework examples, the API reference and the FAQ.
diff --git a/.github/scripts/check_site.py b/.github/scripts/check_site.py
new file mode 100644
index 0000000..dfdd15d
--- /dev/null
+++ b/.github/scripts/check_site.py
@@ -0,0 +1,194 @@
+"""Check the built docs site in a real browser.
+
+Usage: python .github/scripts/check_site.py SITE_DIR AXE_JS
+
+Serves SITE_DIR at http://localhost:8765/jsonrpcserver/, the same path as on
+GitHub Pages, and opens every page in the sitemap, plus the 404 page, in light
+and dark mode at desktop and phone widths. It fails if:
+
+- axe-core finds a serious or critical accessibility problem,
+- a page scrolls sideways, has a JavaScript error, or has no visible h1,
+ description or canonical link,
+- a link to another page of the site, or to an anchor on it, is broken.
+
+Needs playwright (pip install playwright, then playwright install chromium).
+"""
+
+import functools
+import re
+import sys
+import threading
+import urllib.error
+import urllib.request
+from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+from typing import Any, Dict, List, Set, Tuple
+from urllib.parse import urldefrag
+
+from playwright.sync_api import Page, sync_playwright
+
+PORT = 8765
+BASE = f"http://localhost:{PORT}/jsonrpcserver/"
+LIVE = "https://bensynapse.github.io/jsonrpcserver/"
+VIEWPORTS = {"desktop": (1280, 900), "phone": (375, 812)}
+
+
+class Handler(SimpleHTTPRequestHandler):
+ """Serve the site under /jsonrpcserver/, with 404.html for missing pages."""
+
+ def translate_path(self, path: str) -> str:
+ path = path.split("?", 1)[0].split("#", 1)[0]
+ if not path.startswith("/jsonrpcserver/"):
+ return ""
+ return super().translate_path(path[len("/jsonrpcserver") :])
+
+ def send_error(self, code: int, message: Any = None, explain: Any = None) -> None:
+ if code != 404:
+ super().send_error(code, message, explain)
+ return
+ body = (Path(self.directory) / "404.html").read_bytes()
+ self.send_response(404)
+ self.send_header("Content-Type", "text/html")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def log_message(self, format: str, *args: Any) -> None:
+ pass
+
+
+class Server(ThreadingHTTPServer):
+ def handle_error(self, request: Any, client_address: Any) -> None:
+ # The browser often drops a connection it no longer needs.
+ if not isinstance(sys.exc_info()[1], ConnectionError):
+ super().handle_error(request, client_address)
+
+
+def record(errors: List[str], exc: Exception) -> None:
+ errors.append(str(exc))
+
+
+def page_info(page: Page) -> Dict[str, Any]:
+ return page.evaluate(
+ """() => {
+ const q = (s) => document.querySelector(s);
+ const h1 = [...document.querySelectorAll(".md-content h1")]
+ .filter((h) => h.offsetParent !== null);
+ const doc = document.documentElement;
+ return {
+ h1: h1.length,
+ description: (q('meta[name="description"]') || {}).content || "",
+ canonical: (q('link[rel="canonical"]') || {}).href || "",
+ overflow: doc.scrollWidth > doc.clientWidth,
+ links: [...document.querySelectorAll("a[href]")].map((a) => a.href),
+ ids: [...document.querySelectorAll("[id]")].map((e) => e.id),
+ };
+ }"""
+ )
+
+
+def axe_problems(page: Page, axe: str) -> List[str]:
+ page.add_script_tag(content=axe)
+ violations: List[Dict[str, Any]] = page.evaluate(
+ """async () => {
+ const result = await axe.run(document, {
+ runOnly: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "best-practice"],
+ });
+ return result.violations.map((v) => ({
+ id: v.id, impact: v.impact, nodes: v.nodes.map((n) => n.target.join(" ")),
+ }));
+ }"""
+ )
+ return [
+ f"{v['id']} ({v['impact']}): {', '.join(v['nodes'][:3])}"
+ for v in violations
+ if v["impact"] in ("serious", "critical")
+ ]
+
+
+def main(site: Path, axe: str) -> int:
+ handler = functools.partial(Handler, directory=str(site))
+ server = Server(("localhost", PORT), handler)
+ threading.Thread(target=server.serve_forever, daemon=True).start()
+ sitemap = (site / "sitemap.xml").read_text()
+ urls = re.findall(r"(.*?)", sitemap)
+ pages = [url.replace(LIVE, BASE) for url in urls]
+ problems: List[str] = []
+ links: Set[str] = set()
+ ids: Dict[str, Set[str]] = {}
+ with sync_playwright() as playwright:
+ browser = playwright.chromium.launch()
+ for scheme in ("light", "dark"):
+ for name, (width, height) in VIEWPORTS.items():
+ context = browser.new_context(
+ viewport={"width": width, "height": height},
+ color_scheme=scheme,
+ is_mobile=name == "phone",
+ )
+ page = context.new_page()
+ errors: List[str] = []
+ page.on("pageerror", functools.partial(record, errors))
+ for url in [*pages, BASE + "no-such-page/"]:
+ errors.clear()
+ response = page.goto(url, wait_until="networkidle")
+ where = f"{url} ({scheme}, {name})"
+ expected = 404 if url.endswith("no-such-page/") else 200
+ status = response.status if response else None
+ if status != expected:
+ problems.append(f"{where}: status {status}")
+ info = page_info(page)
+ if info["h1"] != 1:
+ problems.append(f"{where}: {info['h1']} visible h1")
+ if not info["description"]:
+ problems.append(f"{where}: no description")
+ if expected == 200 and not info["canonical"]:
+ problems.append(f"{where}: no canonical link")
+ if info["overflow"]:
+ problems.append(f"{where}: the page scrolls sideways")
+ problems.extend(f"{where}: JavaScript error {e}" for e in errors)
+ problems.extend(f"{where}: {p}" for p in axe_problems(page, axe))
+ links.update(info["links"])
+ ids[url] = set(info["ids"])
+ context.close()
+ browser.close()
+ problems.extend(check_links(links, ids))
+ server.shutdown()
+ for problem in problems:
+ print(problem)
+ print(f"{len(pages) + 1} pages, 4 views each, {len(problems)} problems")
+ return 1 if problems else 0
+
+
+def check_links(links: Set[str], ids: Dict[str, Set[str]]) -> List[str]:
+ problems: List[str] = []
+ status: Dict[str, int] = {}
+ for link in sorted(links):
+ # The banner and the 404 page link to the live site's address.
+ url, fragment = urldefrag(link.replace(LIVE, BASE))
+ if not url.startswith(BASE):
+ continue
+ if url not in status:
+ status[url] = fetch_status(url)
+ if status[url] != 200:
+ problems.append(f"broken link: {link} ({status[url]})")
+ elif fragment and url in ids and fragment not in ids[url]:
+ problems.append(f"broken anchor: {link}")
+ return problems
+
+
+def fetch_status(url: str) -> int:
+ try:
+ with urllib.request.urlopen(url, timeout=10) as response:
+ return int(response.status)
+ except urllib.error.HTTPError as exc:
+ return exc.code
+
+
+def parse_args(argv: List[str]) -> Tuple[Path, str]:
+ if len(argv) != 2:
+ raise SystemExit(__doc__)
+ return Path(argv[0]), Path(argv[1]).read_text()
+
+
+if __name__ == "__main__":
+ sys.exit(main(*parse_args(sys.argv[1:])))
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a011fd9..07a673c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -55,7 +55,9 @@ jobs:
- name: No links to hijacked domains anywhere in the repo
run: |
# The second pattern is the previous author's blog, which now redirects to spam.
- if git grep -n -I -i -E 'jsonrpc(client|server)\.com|composed\.blog' -- ':!.github/scripts/check_metadata.py'; then
+ # The FAQ names the old domains, as plain text, so readers recognise them.
+ # tests/test_docs.py checks that they're never links there.
+ if git grep -n -I -i -E 'jsonrpc(client|server)\.com|composed\.blog' -- ':!.github/scripts/check_metadata.py' ':!docs/faq.md'; then
echo "::error::Remove the links above. Those domains no longer belong to the project."
exit 1
fi
@@ -89,16 +91,41 @@ jobs:
- run: mkdocs build --strict
- name: Examples in the README and docs, with every optional library installed
run: |
- for f in README.md docs/*.md; do
+ for f in README.md $(find docs -name '*.md' | sort); do
python tests/doc_examples.py "$f"
done
- name: Start each example server and send it requests
run: python docs/examples/check_examples.py
+ site:
+ # The built docs in a browser: accessibility (axe-core), links and anchors,
+ # and pages that scroll sideways. See .github/scripts/check_site.py.
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: "3.13"
+ - run: python -m pip install -r requirements/docs.txt -r requirements/site.txt
+ - run: python -m playwright install --with-deps chromium
+ - run: mkdocs build --strict
+ - name: Download axe-core and check its hash
+ env:
+ AXE_VERSION: "4.14.0"
+ AXE_SHA512: "9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ=="
+ run: |
+ curl -sSfL -o axe.tgz "https://registry.npmjs.org/axe-core/-/axe-core-$AXE_VERSION.tgz"
+ echo "$AXE_SHA512" | base64 -d > expected.bin
+ openssl dgst -sha512 -binary axe.tgz | cmp - expected.bin
+ tar -xzf axe.tgz package/axe.min.js
+ - run: python .github/scripts/check_site.py site package/axe.min.js
+
ci-ok:
name: CI OK
if: always()
- needs: [test, lint, package, docs]
+ needs: [test, lint, package, docs, site]
runs-on: ubuntu-24.04
steps:
- name: All jobs passed
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 88226b6..293fcf5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -32,8 +32,22 @@ jobs:
echo "Tag $GITHUB_REF_NAME does not match __version__ $version"
exit 1
fi
- - name: CHANGELOG must have an entry for this version
- run: grep -q "^## ${GITHUB_REF_NAME#v}\b" CHANGELOG.md
+ - name: CHANGELOG entry must be dated, and the docs must not call it unreleased
+ run: |
+ version="${GITHUB_REF_NAME#v}"
+ pattern="${version//./\\.}"
+ if ! grep -qE "^## ${pattern} \([0-9]{4}-[0-9]{2}-[0-9]{2}\)$" CHANGELOG.md; then
+ echo "CHANGELOG.md needs a heading like: ## $version (YYYY-MM-DD). See RELEASING.md."
+ exit 1
+ fi
+ if grep -qE "^ *unreleased: \"?${pattern}\"?$" mkdocs.yml; then
+ echo "mkdocs.yml still marks $version as unreleased. See RELEASING.md."
+ exit 1
+ fi
+ if grep -q "isn't released yet" README.md; then
+ echo "README.md still says a version isn't released yet. See RELEASING.md."
+ exit 1
+ fi
- run: python -m build
- run: python -m twine check --strict dist/*
- run: check-wheel-contents dist/*.whl
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50164a9..25a42df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,15 @@
-# jsonrpcserver Change Log
+# Changelog
-## 5.0.10
+## 5.0.10 (not released yet)
The first release since the project moved to
[bensynapse/jsonrpcserver](https://github.com/bensynapse/jsonrpcserver). It
fixes a security problem, so please upgrade. Code that works with 5.0.9 keeps
-working, apart from the security fix and the "Behaviour changes" below.
+working, apart from the security fix and the "Behaviour changes" below. The
+[migration guide](https://bensynapse.github.io/jsonrpcserver/migration/#from-509-to-5010)
+lists what you might notice. Until it's on PyPI, the
+[Security page](https://bensynapse.github.io/jsonrpcserver/security/#if-you-are-on-509)
+shows how to protect a 5.0.9 server.
### Security
@@ -22,9 +26,10 @@ The response now leaves `data` out:
```
The same applies to the -32000 "Server error" response for errors inside
-jsonrpcserver itself. The exception and its traceback are still logged, through
-the `jsonrpcserver.dispatcher` and `jsonrpcserver.async_dispatcher` loggers, so
-you can find them in your server logs.
+jsonrpcserver itself. The exception and its traceback are still logged, on the
+`jsonrpcserver` logger, so you can find them in your server logs. The
+[logging section](https://bensynapse.github.io/jsonrpcserver/errors/#logging)
+of the docs lists its child loggers.
To get the old behaviour back while developing, pass `debug=True` to
`dispatch`, `async_dispatch` or any of the other dispatch functions. Don't turn
@@ -108,6 +113,13 @@ it requests, and runs every code example in the docs. The websockets example
uses the current `websockets.asyncio` API (#287). A new Security page covers
the settings to check before exposing a server.
+The site has an API reference built from the docstrings and a migration
+guide from 4.x. New pages cover errors and logging, notifications and
+batches, context, validation, typing, testing and threads. Each framework has its own
+page, and every example sets `max_batch_size` and a request size limit. The
+examples listen on `localhost:8000`, where the jsonrpcclient examples
+connect.
+
### Packaging
- A wheel is published as well as the source distribution, so installs no
@@ -192,18 +204,24 @@ work in 5.x but give a `DeprecationWarning`, and will be removed in 6.0.
- Add to FAQ.
-## 5.0.3
+## 5.0.3 (Aug 31, 2021)
- Update readme and documentation.
- Internal function `compose` has been replaced with a better one.
-## 5.0.2
+## 5.0.2 (Aug 18, 2021)
- Update readme and setup.py, minor adjustments.
+## 5.0.1 (Aug 18, 2021)
+
+No changelog entry was written for this release. See the `5.0.1` tag.
+
## 5.0.0 (Aug 16, 2021)
-A complete rebuild, with a few important usage changes.
+A complete rebuild, with a few important usage changes. The
+[migration guide](https://bensynapse.github.io/jsonrpcserver/migration/#from-4x-to-5x)
+shows how to update 4.x code.
- Methods must now return a Result (Success or Error).
- The dispatch function now returns a string.
@@ -333,7 +351,7 @@ _The 4.x releases will support Python 3.6+ only._
- Pass some context data through dispatch to the methods.
- Fix not calling notifications in batch requests.
-## 3.4.3 (Jul 13, 2017)
+## 3.4.4 (Jul 13, 2017)
- Fix AttributeError on batch responses
## 3.4.3 (Jul 12, 2017)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8e79632..14e2934 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,10 +36,19 @@ mkdocs serve
```
`tests/test_docs.py` runs every Python block in the README and docs, so keep
-the shown output accurate. The framework examples are files in
-`docs/examples/`. To run them against real servers, install
-`requirements/examples.txt` and run `python docs/examples/check_examples.py`.
-They listen on port 5000, so free it first.
+the shown output accurate. `tests/doc_examples.py` explains the markers that
+skip a block or start the quickstart server for it. Blocks that need a
+framework are skipped unless it's installed, so to run them all, install
+`requirements/examples.txt` first, as CI's docs job does.
+
+The framework examples are files in `docs/examples/`. To start each one and
+send it real requests, install `requirements/examples.txt` and run
+`python docs/examples/check_examples.py`. It also needs curl. The examples
+listen on `localhost:8000`, so free that port first.
+
+The API reference is built from the docstrings with mkdocstrings. Use Google
+style, and add every new public name to `docs/reference.md`. A test checks
+that every name in `__all__` is there.
## Pull requests
diff --git a/README.md b/README.md
index 0edf0cc..b55fbfb 100644
--- a/README.md
+++ b/README.md
@@ -1,63 +1,124 @@
-
+
-
-
-
+
+
+
-
+
- Process incoming JSON-RPC requests in Python
+ Process incoming JSON-RPC 2.0 requests in Python
Documentation |
+ API reference |
Examples |
- Changelog
+ Changelog |
+ Migration
-https://github.com/user-attachments/assets/94fb4f04-a5f1-41ca-84dd-7e18b87990e0
-
-## Installation
+jsonrpcserver takes a [JSON-RPC 2.0](https://www.jsonrpc.org/specification)
+request, calls your Python function and gives you the response to send back.
+It leaves the networking to you, so it fits into whatever server you already
+have.
+
+## Features
+
+- Works with any framework or transport. The docs have tested examples for
+ http.server, Flask, Werkzeug, Django, FastAPI, aiohttp, Sanic, Tornado,
+ websockets, ZeroMQ and Socket.IO.
+- Sync and async: `dispatch` and `async_dispatch`. With `async_dispatch`, the
+ requests in a batch run concurrently.
+- Follows the spec for batches, notifications and errors, and checks each
+ request's params against your function's signature.
+- Keeps exception messages out of responses and in your logs, and limits
+ batch size with `max_batch_size` (from 5.0.10).
+- Typed: ships `py.typed`, and `@method` keeps your functions' signatures for
+ mypy and pyright.
+- Safe to call from several threads, and tested on Python 3.8 to 3.14,
+ including free-threaded 3.14t.
+
+## Install
```sh
pip install jsonrpcserver
```
-It supports Python 3.8 and later.
+The latest release on PyPI is 5.0.9. This README and the documentation
+describe 5.0.10, which isn't released yet. 5.0.9 sends exception messages to
+clients. The
+[Security page](https://bensynapse.github.io/jsonrpcserver/security/#if-you-are-on-509)
+shows how to stop that, and the
+[changelog](https://bensynapse.github.io/jsonrpcserver/changelog/) lists the
+other differences.
+
+## Quickstart
-## Usage
+Save this as `server.py` and run it with `python server.py`:
+
```python
-from jsonrpcserver import Result, Success, dispatch, method
+from jsonrpcserver import Result, Success, method, serve
@method
def ping() -> Result:
return Success("pong")
+
+
+if __name__ == "__main__":
+ serve("localhost", 8000)
```
-```python
->>> dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}')
-'{"jsonrpc": "2.0", "result": "pong", "id": 1}'
+Then send it a request from another terminal:
+
+```sh
+curl -s -H 'Content-Type: application/json' -d '{"jsonrpc": "2.0", "method": "ping", "id": 1}' http://localhost:8000/
```
-jsonrpcserver doesn't listen on a port itself. The
-[examples](https://bensynapse.github.io/jsonrpcserver/examples/) show it with
-Flask, FastAPI, Django, aiohttp, websockets, ZeroMQ and more.
+Output:
-Before you expose a server to the internet, read the
-[security notes](https://bensynapse.github.io/jsonrpcserver/security/).
+```text
+{"jsonrpc": "2.0", "result": "pong", "id": 1}
+```
+
+`serve()` is a small server for trying things out. In your own framework,
+pass the request body to `dispatch` and send back the string it returns. The
+[examples](https://bensynapse.github.io/jsonrpcserver/examples/) show how.
+
+## Security
+
+Before you put a server on the internet, pass `max_batch_size` to `dispatch`
+and limit the request body size in your framework. Upgrade to 5.0.10 when
+it's out. The
+[Security page](https://bensynapse.github.io/jsonrpcserver/security/)
+explains why.
## Documentation
-Full documentation is at
-[bensynapse.github.io/jsonrpcserver](https://bensynapse.github.io/jsonrpcserver/).
+- [Documentation](https://bensynapse.github.io/jsonrpcserver/): guides,
+ framework examples and the
+ [API reference](https://bensynapse.github.io/jsonrpcserver/reference/).
+- [Migrating from 4.x](https://bensynapse.github.io/jsonrpcserver/migration/):
+ methods must now return `Success(value)`. A 4.x method that returns a plain
+ value still runs, but the client gets an Internal error.
+- [Contributing](https://github.com/bensynapse/jsonrpcserver/blob/main/CONTRIBUTING.md)
+ and the
+ [security policy](https://github.com/bensynapse/jsonrpcserver/blob/main/SECURITY.md).
+- [License](https://github.com/bensynapse/jsonrpcserver/blob/main/LICENSE): MIT.
## See also
-- [jsonrpcclient](https://github.com/bensynapse/jsonrpcclient): create JSON-RPC requests and parse responses in Python
+[jsonrpcclient](https://bensynapse.github.io/jsonrpcclient/)
+([GitHub](https://github.com/bensynapse/jsonrpcclient)) creates JSON-RPC
+requests and parses the responses in Python. It's the client-side companion
+to this library, and its quickstart talks to the server above.
+
+## Credits
+
+Created by Beau Barker. Maintained by [Synapse Research](https://synapsereality.io).
diff --git a/RELEASING.md b/RELEASING.md
index a5165d3..ef7fe1c 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -29,9 +29,23 @@ Settings → Environments → pypi.
## Each release
-1. Merge a pull request that sets `__version__` in `jsonrpcserver/__init__.py`
- and adds a `## ` section to CHANGELOG.md. The release workflow
- fails if either is missing or they don't match the tag.
+1. Merge a pull request that gets the version ready:
+ - `__version__` in `jsonrpcserver/__init__.py` is the new version.
+ - Its CHANGELOG.md heading has the release date instead of "not released
+ yet": `## 5.0.10 (2026-10-20)`.
+ - The `unreleased` and `pypi_version` lines under `extra` in mkdocs.yml are
+ deleted. They show the "not on PyPI yet" banner on every docs page.
+ Merging this redeploys the docs, so merge it just before you tag.
+ - The paragraph under "Install" in README.md that says the version isn't
+ released yet is deleted. The README becomes the PyPI page.
+ - For 5.0.10 only: shorten the "If you are on 5.0.9" section of
+ `docs/security.md` to a note for people who can't upgrade. The "when it's
+ out" wording in README.md and SECURITY.md can go too. `grep -rn "when it's out\|isn't on PyPI" README.md
+ SECURITY.md docs` finds them.
+
+ The release workflow checks the first three. It stops if the version and
+ tag don't match or the CHANGELOG heading has no date. It also stops if
+ mkdocs.yml or README.md still say the version isn't released.
2. Tag the merge commit on main and push the tag:
```sh
@@ -50,6 +64,14 @@ Settings → Environments → pypi.
The release should have two files, a `.tar.gz` and a `.whl`.
+5. If the release fixes a security problem, publish a repository security
+ advisory for it, so Dependabot, `pip-audit` and OSV warn people on older
+ versions. In the Security tab, choose "New draft security advisory". Set
+ the affected versions (for 5.0.10: `<= 5.0.9`) and the patched version.
+ Pick the CWE, link the docs page that explains it, and publish. For the
+ 5.0.10 fix, the CWE is CWE-209, information exposure through an error
+ message.
+
## If the upload fails
- `invalid-publisher`: the PyPI publisher settings above don't match. Check
@@ -65,4 +87,6 @@ whose version reached PyPI.
## Docs
The docs site deploys itself from main through `.github/workflows/docs.yml`,
-so it changes when the docs change, not at release time.
+so it changes when the docs change, not at release time. While main is ahead
+of PyPI, the `unreleased` setting in mkdocs.yml shows a banner that says so.
+The docs also mark features with "New in" or "Changed in" notes.
diff --git a/SECURITY.md b/SECURITY.md
index 4ccaa45..b38006a 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -14,9 +14,10 @@ problem. We'll reply within a week.
Fixes go into the latest 5.x release. Older releases don't get updates.
Versions before 5.0.10 send the message of any uncaught exception in a method
-to the client. Please upgrade. The
-[security notes](https://bensynapse.github.io/jsonrpcserver/security/) in the
-docs explain this and the other settings to check before exposing a server.
+to the client. Please upgrade when 5.0.10 is on PyPI. Until then, the
+[security notes](https://bensynapse.github.io/jsonrpcserver/security/#if-you-are-on-509)
+in the docs show how to protect a 5.0.9 server. They also cover the other
+settings to check before exposing a server.
## Old domains
diff --git a/docs/assets/a11y.js b/docs/assets/a11y.js
new file mode 100644
index 0000000..a752b79
--- /dev/null
+++ b/docs/assets/a11y.js
@@ -0,0 +1,27 @@
+// Small accessibility fixes for Material for MkDocs defaults. They run after
+// Material has rendered each page, including after instant navigation.
+document$.subscribe(() => {
+ // The search overlay is a dialog, so it needs a name.
+ const search = document.querySelector(".md-search");
+ if (search && !search.hasAttribute("aria-label")) {
+ search.setAttribute("aria-label", "Search");
+ }
+ document.querySelectorAll(".md-code__nav").forEach((nav, i) => {
+ // Each code block's toolbar is a nav landmark; give each a distinct name.
+ nav.setAttribute("aria-label", `Code block ${i + 1} actions`);
+ });
+ document.querySelectorAll(".highlight pre > code").forEach((code) => {
+ // A code block that scrolls sideways must be reachable with the keyboard.
+ if (code.scrollWidth > code.clientWidth) {
+ code.setAttribute("tabindex", "0");
+ }
+ });
+ document.querySelectorAll(".md-typeset__scrollwrap").forEach((wrap, i) => {
+ // So must a table that's wider than the screen.
+ if (wrap.scrollWidth > wrap.clientWidth) {
+ wrap.setAttribute("tabindex", "0");
+ wrap.setAttribute("role", "region");
+ wrap.setAttribute("aria-label", `Table ${i + 1}, scrolls sideways`);
+ }
+ });
+});
diff --git a/docs/assets/apple-touch-icon.png b/docs/assets/apple-touch-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..fac09450bc5bd0afe5fa14b5e559bca5b3f4f610
GIT binary patch
literal 7388
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
n_*$rR$D3DSNH&keM>> " and "... " prompts are removed and output lines are left
+// out, so the result can be pasted into a file or a terminal.
+document.addEventListener(
+ "click",
+ (event) => {
+ const button = event.target.closest("[data-clipboard-target]");
+ if (!button) return;
+ const target = document.querySelector(button.dataset.clipboardTarget);
+ if (!target || !target.closest(".language-pycon")) return;
+ const code = target.textContent
+ .split("\n")
+ .filter((line) => /^(>>>|\.\.\.)( |$)/.test(line))
+ .map((line) => line.slice(4))
+ .join("\n");
+ button.setAttribute("data-clipboard-text", code + "\n");
+ },
+ true,
+);
diff --git a/docs/assets/extra.css b/docs/assets/extra.css
index 26e3b96..dc7db7f 100644
--- a/docs/assets/extra.css
+++ b/docs/assets/extra.css
@@ -1,106 +1,117 @@
+/* System fonts instead of web fonts (theme.font is false in mkdocs.yml). */
:root {
- /* ✅ Fonts */
- --md-text-font: -apple-system, BlinkMacSystemFont, "Segoe UI Adjusted",
- "Segoe UI", "Liberation Sans", sans-serif;
+ --md-text-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Liberation Sans",
+ Roboto, sans-serif;
--md-code-font: "SF Mono", SFMono-Regular, ui-monospace, "DejaVu Sans Mono",
Menlo, Consolas, monospace;
+}
- /* ✅ Light Mode Text Colors */
+/* Code colours with at least 4.5:1 contrast on the code background. Material's
+ defaults for comments, output and a few token types fall just short. */
+[data-md-color-scheme="default"] {
--md-default-fg-color: #111;
+ --md-code-hl-comment-color: #5e5e5e;
+ --md-code-hl-generic-color: #5e5e5e;
+ --md-code-hl-operator-color: #5e5e5e;
+ --md-code-hl-variable-color: #5e5e5e;
+}
+
+/* Pink is this site's colour (jsonrpcclient's is indigo). Material's pink,
+ #e92063, gives white text 4.3:1 and fails WCAG AA, so use a darker one:
+ #c2185b is 5.9:1 against white either way round. */
+[data-md-color-primary="pink"] {
+ --md-primary-fg-color: #c2185b;
+ --md-primary-fg-color--light: #d81b60;
+ --md-primary-fg-color--dark: #ad1457;
+}
+
+[data-md-color-accent="pink"] {
+ --md-accent-fg-color: #ad1457;
+ --md-accent-fg-color--transparent: rgba(173, 20, 87, 0.1);
+}
+
+/* Material sets the link colour per scheme and primary colour, so match those
+ selectors. */
+[data-md-color-scheme="default"][data-md-color-primary="pink"] {
+ --md-typeset-a-color: #c2185b;
+}
+
+[data-md-color-scheme="slate"][data-md-color-primary="pink"] {
+ --md-typeset-a-color: #f48fb1;
+}
+
+[data-md-color-scheme="slate"][data-md-color-accent="pink"] {
+ --md-accent-fg-color: #f8bbd0;
+ --md-accent-fg-color--transparent: rgba(248, 187, 208, 0.1);
}
[data-md-color-scheme="slate"] {
- /* ✅ Dark Mode Text Colors */
--md-default-fg-color: #e9e9e9;
+ --md-code-hl-comment-color: #a8a8b3;
+ --md-code-hl-generic-color: #a8a8b3;
+ --md-code-hl-number-color: #f39287;
+ --md-code-hl-constant-color: #ab9ff0;
+}
+
+/* The repository's star and fork counts in the header are faded by default,
+ which leaves them below 4.5:1 on the pink header. */
+.md-source__facts {
+ opacity: 1;
+}
+
+/* The "not on PyPI yet" banner sits on a dark background in both schemes. */
+.md-banner a,
+.md-banner a:hover,
+.md-banner a:focus {
+ color: #fff;
+ text-decoration: underline;
+}
+
+.md-banner code {
+ background-color: transparent;
+ color: inherit;
}
-/* Fix a horizontal scroll isue */
-.md-typeset * {
- margin-left: 0 !important;
- margin-right: 0 !important;
+/* The footer's copyright text, which is too faint by default. */
+.md-footer-meta {
+ --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.75);
+}
+
+.md-copyright a {
+ text-decoration: underline;
}
.md-typeset {
- margin-left: 0 !important;
- margin-right: 0 !important;
-
- p,
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- margin: 0;
- padding: 0;
- padding-left: 20px;
- padding-right: 20px;
- }
-
- /* Change header fonts */
- h1, h2, h3, h4, h5, h6 {
- font-weight: 700;
- font-style: normal;
- letter-spacing: -0.5px;
- line-height: 140%;
- }
-
- h1 {
- color: var(--md-default-fg-color);
- font-size: 1.2rem;
- margin-bottom: 1em;
- }
-
- h2 {
- font-size: 1rem;
- }
-
- h3 {
- font-size: 0.9rem;
- }
-
- p, ul, ol, pre {
- font-size: 17px;
- margin-top: 1em;
- margin-bottom: 1em;
- }
-
- ul, ol {
- margin-left: 3rem;
- padding-left: 3rem;
- }
-
- blockquote {
- border-left: 0 !important;
- border-radius: 0;
- margin: 0;
- margin-bottom: 1em;
- padding: 1px;
- background-color: rgba(255, 87, 34, 0.05);
-
- [data-md-color-scheme="slate"] {
- background-color: rgba(255, 204, 0, 0.1);
- }
- }
-
- pre {
- line-height: 150%;
- }
- pre > code {
- padding-left: 20px;
- }
-}
-
-.md-content__inner {
- /* Target only the page title (first H1 in the article) */
- .md-typeset {
- h1:first-of-type {
- padding-left: 20px;
- }
- }
-}
-
-article {
- padding-bottom: 3rem;
+ font-size: 0.85rem;
+}
+
+.md-typeset h1,
+.md-typeset h2,
+.md-typeset h3 {
+ font-weight: 700;
+ letter-spacing: -0.01em;
}
+.md-typeset h1 {
+ color: var(--md-default-fg-color);
+}
+
+/* Underline links in running text, so they don't rely on colour alone
+ (WCAG 1.4.1). Navigation, buttons and permalinks keep their own style. */
+.md-typeset p a,
+.md-typeset li a,
+.md-typeset td a {
+ text-decoration: underline;
+ text-underline-offset: 0.15em;
+}
+
+.md-typeset blockquote {
+ border-left: 0.2rem solid rgb(255, 87, 34);
+ background-color: rgba(255, 87, 34, 0.05);
+ color: var(--md-default-fg-color);
+}
+
+[data-md-color-scheme="slate"] .md-typeset blockquote {
+ border-left-color: rgb(255, 204, 0);
+ background-color: rgba(255, 204, 0, 0.1);
+}
diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..0170a2d33bf34c33098e6f363563f4ea6c0ebf95
GIT binary patch
literal 2408
zcmV-u377VXP)xRCM;#M6}Dvn!%MGNV;tW^*r>`{534d10pSE(3ThDy>=R)R%1)p1
zVCmrY_a@p_Uow^PJuAL&TDuf(OVR6!VwZZ9SUoAq$*>$kP7DU2qS--*JkqZtzwSKn
z{D%`kkcr0kcU*RLLEOE0ai>;%N{Y|IaUv(G=ckGUPzBUsY|%KXn(iOb^oI7{=l(qw
z1Q~1mz>1AGwwm1CF&S0WRKT7rMjr_bg)A@Pa#+ef-n!(#&U@-M%c$pe9wVTt+)*@9XN+bG)%cL_Vq!Eany6EiI|NjQmVIfpTKAekTGJD_#5I!>9D+y7
zlBMUG%tl*E;@VQYU+BiySYJ}SFNtf*Y_!FoexnA@FPbHR4?z7!19E<>9$mn>KRFG=
z#OeV#-#C$#$s)-8iD{6aIFXemMRhTS|JXE0Q8kOn^HW8Fo0lsa1BX)%X?Emut(|vbLlrbmz)yH!5v08}fO9y4I(P_DU#IHuYYapVDHgyP$ovTifHmmg
zFcLY;`m-@>&%mtefI_ZuOf@07=TUU159IcskE@0-RfQ}Izb1e*mDk5%@X;Y;?X82v`%q
zG=f?CDGK-929YCq@JXb-ZO-Y?wB{#&>koqQN!oaDXCa@1LLPDvw6`5G{{dLi5{@nA
z!}TH;t|bx(RF(YgHzCX0A<2$@y?KoQBT%Vg&su}MaBc9ev!mgRD#^o7lRf(q+FuU&
zJTMfFnJN-QsNVEDXe}YFMa+hEMBlp>DduZWYr%VDRR?y%I?|_J2z-#~aRNXDO_Gqa
z`3SrQP_hiHC3)aUM!tOu6iSW4+fqED*U}euhp;w+-uEwb|3~C+ziITEbzseztKsqE
z9RW2@OG~lgb!2qZm~Hxx(4+e&7&tqlY&`lT`}ZleAhPg;2^SeNdRT`(@WyNK1}hl3qo65j{;Sq8?M1!
zw+a8oyFqFGb34i39}SXwnq}9`)cbPUa_ps_9bf9|a{$1<{todU?k9cZ*;<>L%|1ah
zk1S~+d-_Ex*Z!2;FTRVt4zs$G=ttM%uK8jE
zgft66rmKgc4c`eVFxygS3_3VO>G3}YjamkAy2!y$1AkDDn8ucVAkTHU;;^MD0Eh_<&$uyF_;uG`ft2!SPaoQQ5bf1Tet?#Tky0@{U9CHGS6oc##ea~!up2!z2$gtxTVtKe
zkd^}Gf^)DN){RLgMmJskYwsXY)BxtV0H6wm9DeTyq)+{a=&P590cPYz+f{SU>6mj)
zZ;aC7TL>tpj4Qh)NbxbOp#)q_*Zw~-+C|T|FLf!q*)l8#7PM2
zX>5%VCoobC2J4sxEEl1D15|(Vdo)RFD)e}BfLF-p@cVih{_@Sle|ZT1c2`50)wjoH
zx;0kFhe$F(_G(u*$^B0-eAN%}FTWP{BAP&iGZRBc8(+lowYgm>KV=U;?X9tieN}zSy~%5S-Vsu*k<%HT;}0fU`gV1BbX(ClQOFj7_p_pL;?Zp_eY?7hh7SU7sraNPhge;}
zT%f4WA-3X^9uaO)5CfY{ThET}IMXc!v0@SR3lId4dW&d5ti+jaY3te14V#S-wq?p@
z+upb1nL!_KEsC>wF~&o>5K3e19rH9V#uUZbK_71&{brDD*~g#Gjz4h`e>^=A1XvuO
a&;K8lb)PN%CA{wd0000
literal 0
HcmV?d00001
diff --git a/docs/assets/social-card.png b/docs/assets/social-card.png
new file mode 100644
index 0000000000000000000000000000000000000000..f6d8bb300c1592296fc6958c2752302527b997d7
GIT binary patch
literal 67107
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