mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 14:50:19 +00:00
90a082a0ac
## Summary
New `enable_logging=True` parameter on `responder.API()` that provides
structured, request-scoped logging using stdlib `logging` and
`contextvars`.
### What it does
- **`api.log`** — always available on every API instance. Works as a
plain logger by default; gains per-request context enrichment with
`enable_logging=True`
- **Per-request context** — every log message automatically includes
request ID, HTTP method, path, and client IP
- **Access logging** — logs every request with timing: `GET /path → 200
(1.2ms)`
- **Request ID** — generates or forwards `X-Request-ID` headers
(supersedes `request_id=True` when both are set)
- **stdlib logging** — works with any existing handler, formatter, or
log aggregator
### Usage
```python
# api.log always works — no setup required
api = responder.API()
api.log.info("starting up") # plain logger, no context
# With enable_logging=True, log messages get request context automatically
api = responder.API(enable_logging=True)
@api.route("/")
def index(req, resp):
api.log.info("handling request")
# => 2026-03-24 12:00:00 [INFO] responder.app — handling request [GET /] [req:abc123] [client:127.0.0.1]
```
For additional loggers in helper modules:
```python
from responder.ext.logging import get_logger
logger = get_logger("myapp.db")
```
### Architecture
- `responder/ext/logging.py` — self-contained module with:
- `LoggingMiddleware` — pure ASGI middleware that sets contextvars and
logs access
- `RequestContextFilter` — logging filter that injects context into
records
- `RequestContext` — read-only access to current request metadata
- `get_logger()` / `setup_logging()` — convenience functions
- `api.log` — always a valid logger; context-aware when
`enable_logging=True`, plain stdlib logger otherwise
- Wired into `API.__init__` via the `enable_logging` parameter
### Files
- `responder/ext/logging.py` — new module
- `responder/api.py` — added `enable_logging` parameter and `api.log`
- `tests/test_logging.py` — 9 tests
- `docs/source/tour.rst` — new Structured Logging section
- `docs/source/index.rst` — added to feature list
## Test plan
- [x] 9 logging tests pass
- [x] Full suite: 208 passed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
739 lines
23 KiB
ReStructuredText
739 lines
23 KiB
ReStructuredText
Feature Tour
|
|
============
|
|
|
|
This section walks through Responder's features in depth. Each section
|
|
explains the concept, shows working code, and explains the design choices
|
|
behind it. If you're new to web development, this is a good place to learn
|
|
how modern web frameworks work under the hood.
|
|
|
|
|
|
Method Filtering
|
|
----------------
|
|
|
|
HTTP defines several *methods* (also called verbs) that describe what a
|
|
client wants to do with a resource. The most common are:
|
|
|
|
- ``GET`` — retrieve data
|
|
- ``POST`` — create something new
|
|
- ``PUT`` — replace something entirely
|
|
- ``PATCH`` — update part of something
|
|
- ``DELETE`` — remove something
|
|
|
|
By default, a Responder route matches all methods. This is fine for simple
|
|
endpoints, but REST APIs typically map different methods to different
|
|
operations. Use the ``methods`` parameter to restrict a route::
|
|
|
|
@api.route("/items", methods=["GET"])
|
|
def list_items(req, resp):
|
|
resp.media = {"items": []}
|
|
|
|
@api.route("/items", methods=["POST"], check_existing=False)
|
|
async def create_item(req, resp):
|
|
data = await req.media()
|
|
resp.media = {"created": data}
|
|
|
|
Note the ``check_existing=False`` — Responder normally prevents you from
|
|
registering two routes with the same path (to catch typos). When you
|
|
intentionally want multiple handlers for the same path with different
|
|
methods, you need to opt in.
|
|
|
|
|
|
Class-Based Views
|
|
-----------------
|
|
|
|
Function-based views are great for simple endpoints, but sometimes you want
|
|
to group related HTTP methods together into a single resource. This is
|
|
where class-based views come in — a pattern popularized by
|
|
`Falcon <https://falconframework.org/>`_.
|
|
|
|
Responder dispatches to the appropriate method handler based on the HTTP
|
|
method::
|
|
|
|
@api.route("/{greeting}")
|
|
class GreetingResource:
|
|
def on_get(self, req, resp, *, greeting):
|
|
resp.text = f"{greeting}, world!"
|
|
|
|
def on_post(self, req, resp, *, greeting):
|
|
resp.media = {"received": greeting}
|
|
|
|
def on_request(self, req, resp, *, greeting):
|
|
"""Called on EVERY request, before the method-specific handler."""
|
|
resp.headers["X-Greeting"] = greeting
|
|
|
|
The ``on_request`` method is called for all HTTP methods, much like
|
|
middleware scoped to a single route. Method-specific handlers (``on_get``,
|
|
``on_post``, ``on_put``, ``on_delete``, etc.) are called after.
|
|
|
|
No inheritance required — just define a class with the right method names.
|
|
This is simpler than Django's ``View`` classes and more Pythonic than
|
|
framework-specific base classes.
|
|
|
|
|
|
Lifespan Events
|
|
---------------
|
|
|
|
Real applications need to set up resources when they start (database
|
|
connection pools, ML models, caches) and tear them down when they stop.
|
|
This is called the application *lifespan*.
|
|
|
|
The modern approach is the *context manager* pattern, where startup and
|
|
shutdown are two halves of the same block::
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app):
|
|
# Startup — runs before the first request
|
|
print("connecting to database...")
|
|
yield
|
|
# Shutdown — runs after the server stops
|
|
print("closing connections...")
|
|
|
|
api = responder.API(lifespan=lifespan)
|
|
|
|
Everything before ``yield`` runs at startup. Everything after runs at
|
|
shutdown. If startup fails, the server won't start. If shutdown raises,
|
|
it's logged but the server still exits.
|
|
|
|
The traditional event decorator style also works::
|
|
|
|
@api.on_event("startup")
|
|
async def startup():
|
|
print("starting up")
|
|
|
|
@api.on_event("shutdown")
|
|
async def shutdown():
|
|
print("shutting down")
|
|
|
|
The context manager is preferred for new code — it keeps related startup
|
|
and shutdown logic together and makes resource cleanup more explicit.
|
|
|
|
|
|
Serving Files
|
|
-------------
|
|
|
|
Web applications often need to serve files — downloads, reports, images.
|
|
Responder makes this simple with ``resp.file()``, which reads a file from
|
|
disk and sets the ``Content-Type`` header automatically using Python's
|
|
``mimetypes`` module::
|
|
|
|
@api.route("/download")
|
|
def download(req, resp):
|
|
resp.file("reports/annual.pdf")
|
|
|
|
You can override the content type if the automatic detection isn't right::
|
|
|
|
@api.route("/image")
|
|
def image(req, resp):
|
|
resp.file("photos/cat.jpg", content_type="image/jpeg")
|
|
|
|
For large files, use ``resp.stream_file()`` to avoid loading the entire
|
|
file into memory. This streams the file in chunks::
|
|
|
|
@api.route("/export")
|
|
def export(req, resp):
|
|
resp.stream_file("data/export.csv")
|
|
|
|
|
|
Custom Error Handling
|
|
---------------------
|
|
|
|
In production, you don't want your users to see raw Python tracebacks.
|
|
Responder lets you register custom handlers for specific exception types,
|
|
so you can return clean, structured error responses::
|
|
|
|
@api.exception_handler(ValueError)
|
|
async def handle_value_error(req, resp, exc):
|
|
resp.status_code = 400
|
|
resp.media = {"error": str(exc)}
|
|
|
|
Now, any route that raises a ``ValueError`` will return a clean JSON
|
|
response with a 400 status code instead of a generic 500 error page.
|
|
|
|
This is a common pattern in API development — you define your own exception
|
|
classes for different error conditions, register handlers for each, and
|
|
your API always returns consistent, machine-readable error responses.
|
|
|
|
|
|
Before-Request Hooks
|
|
--------------------
|
|
|
|
Sometimes you need to run the same code before every request —
|
|
authentication checks, request logging, adding common headers, or setting
|
|
up per-request state. Before-request hooks let you do this without
|
|
duplicating code in every route::
|
|
|
|
@api.route(before_request=True)
|
|
def add_headers(req, resp):
|
|
resp.headers["X-API-Version"] = "3.2"
|
|
|
|
**Short-circuiting** is the really powerful part. If your hook sets
|
|
``resp.status_code``, the route handler is skipped entirely and the
|
|
response is sent immediately. This is the pattern for authentication::
|
|
|
|
@api.route(before_request=True)
|
|
def auth_check(req, resp):
|
|
if "Authorization" not in req.headers:
|
|
resp.status_code = 401
|
|
resp.media = {"error": "unauthorized"}
|
|
|
|
If the ``Authorization`` header is missing, the client gets a 401 response
|
|
and the actual route handler never runs. This is cleaner than adding
|
|
auth checks to every individual route.
|
|
|
|
|
|
After-Request Hooks
|
|
-------------------
|
|
|
|
The complement to before-request hooks. After-request hooks run after the
|
|
route handler completes but before the response is sent. They're useful
|
|
for logging, adding response headers, or any post-processing::
|
|
|
|
@api.after_request()
|
|
def log_response(req, resp):
|
|
print(f"{req.method} {req.full_url} -> {resp.status_code}")
|
|
|
|
@api.after_request()
|
|
async def add_timing(req, resp):
|
|
resp.headers["X-Served-By"] = "responder"
|
|
|
|
|
|
WebSocket Support
|
|
-----------------
|
|
|
|
HTTP is a request-response protocol — the client asks, the server answers.
|
|
But some applications need real-time, bidirectional communication: chat
|
|
apps, live dashboards, multiplayer games, collaborative editors.
|
|
|
|
`WebSockets <https://en.wikipedia.org/wiki/WebSocket>`_ solve this by
|
|
upgrading an HTTP connection into a persistent, full-duplex channel where
|
|
both sides can send messages at any time::
|
|
|
|
@api.route("/ws", websocket=True)
|
|
async def websocket(ws):
|
|
await ws.accept()
|
|
while True:
|
|
name = await ws.receive_text()
|
|
await ws.send_text(f"Hello {name}!")
|
|
await ws.close()
|
|
|
|
You can send and receive in multiple formats:
|
|
|
|
- ``send_text`` / ``receive_text`` — plain text strings
|
|
- ``send_json`` / ``receive_json`` — JSON objects (auto-serialized)
|
|
- ``send_bytes`` / ``receive_bytes`` — raw binary data
|
|
|
|
WebSocket routes are marked with ``websocket=True`` in the route decorator.
|
|
They receive a ``ws`` object instead of ``req`` and ``resp``.
|
|
|
|
|
|
Server-Sent Events (SSE)
|
|
-------------------------
|
|
|
|
SSE is a simpler alternative to WebSockets for *one-way* real-time
|
|
communication — the server pushes events to the client, but the client
|
|
can't send messages back. This is perfect for live feeds, progress bars,
|
|
notification streams, and AI response streaming.
|
|
|
|
Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected
|
|
by the browser, and doesn't require any special client-side libraries::
|
|
|
|
@api.route("/events")
|
|
async def events(req, resp):
|
|
@resp.sse
|
|
async def stream():
|
|
for i in range(10):
|
|
yield {"data": f"message {i}"}
|
|
|
|
On the client side, you consume SSE events with JavaScript's built-in
|
|
``EventSource`` API::
|
|
|
|
const source = new EventSource("/events");
|
|
source.onmessage = (event) => {
|
|
console.log(event.data);
|
|
};
|
|
|
|
Each yielded value can be a string (treated as data) or a dict with the
|
|
standard SSE fields::
|
|
|
|
yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"}
|
|
yield "simple string message"
|
|
|
|
|
|
GraphQL
|
|
-------
|
|
|
|
`GraphQL <https://graphql.org/>`_ is a query language for APIs that lets
|
|
clients request exactly the data they need — no more, no less. Instead of
|
|
multiple REST endpoints, you define a schema and let clients query it.
|
|
|
|
Responder includes built-in GraphQL support via
|
|
`Graphene <https://graphene-python.org/>`_. Set up a full GraphQL endpoint
|
|
with a single method call::
|
|
|
|
import graphene
|
|
|
|
class Query(graphene.ObjectType):
|
|
hello = graphene.String(name=graphene.String(default_value="stranger"))
|
|
|
|
def resolve_hello(self, info, name):
|
|
return f"Hello {name}"
|
|
|
|
api.graphql("/graphql", schema=graphene.Schema(query=Query))
|
|
|
|
Visiting ``/graphql`` in a browser renders the
|
|
`GraphiQL <https://github.com/graphql/graphiql>`_ interactive IDE, where
|
|
you can explore your schema, write queries, and see results in real-time.
|
|
Programmatic clients can POST JSON queries to the same endpoint.
|
|
|
|
You can access the Responder request and response objects in your resolvers
|
|
through ``info.context["request"]`` and ``info.context["response"]``.
|
|
|
|
|
|
OpenAPI Documentation
|
|
---------------------
|
|
|
|
`OpenAPI <https://www.openapis.org/>`_ (formerly Swagger) is the industry
|
|
standard for describing REST APIs. An OpenAPI specification lets you
|
|
auto-generate interactive documentation, client libraries, and validation
|
|
logic.
|
|
|
|
Responder generates OpenAPI specs from your code::
|
|
|
|
api = responder.API(
|
|
title="Pet Store",
|
|
version="1.0",
|
|
openapi="3.0.2",
|
|
docs_route="/docs",
|
|
)
|
|
|
|
This gives you:
|
|
|
|
- An OpenAPI schema at ``/schema.yml``
|
|
- Interactive Swagger UI documentation at ``/docs``
|
|
|
|
There are three ways to document your endpoints.
|
|
|
|
**Pydantic models** — the recommended approach. Use ``request_model`` and
|
|
``response_model`` to annotate your routes, and Responder generates the
|
|
schema automatically. When ``request_model`` is set, request bodies are
|
|
also validated automatically — invalid inputs get a ``422`` response with
|
|
detailed error messages::
|
|
|
|
from pydantic import BaseModel
|
|
|
|
class PetIn(BaseModel):
|
|
name: str
|
|
age: int = 0
|
|
|
|
class PetOut(BaseModel):
|
|
id: int
|
|
name: str
|
|
age: int
|
|
|
|
@api.route("/pets", methods=["POST"],
|
|
request_model=PetIn, response_model=PetOut)
|
|
async def create_pet(req, resp):
|
|
data = await req.media()
|
|
resp.media = {"id": 1, **data}
|
|
|
|
When ``response_model`` is set, the response is serialized through the
|
|
model — extra fields are stripped and types are enforced.
|
|
|
|
**YAML docstrings** — for full control, embed OpenAPI YAML in the
|
|
docstring::
|
|
|
|
@api.route("/pets")
|
|
def list_pets(req, resp):
|
|
"""A list of pets.
|
|
---
|
|
get:
|
|
description: Get all pets
|
|
responses:
|
|
200:
|
|
description: A list of pets
|
|
"""
|
|
resp.media = [{"name": "Fido"}]
|
|
|
|
**Marshmallow schemas** — if you're already using marshmallow::
|
|
|
|
from marshmallow import Schema, fields
|
|
|
|
@api.schema("Pet")
|
|
class PetSchema(Schema):
|
|
name = fields.Str()
|
|
|
|
All three approaches can be mixed in the same API. You can choose from
|
|
multiple documentation themes: ``swagger_ui`` (default), ``redoc``,
|
|
``rapidoc``, or ``elements``.
|
|
|
|
|
|
Route Groups
|
|
------------
|
|
|
|
As your application grows, you'll want to organize routes logically.
|
|
Route groups let you share a URL prefix across related endpoints — a
|
|
common pattern for API versioning::
|
|
|
|
v1 = api.group("/v1")
|
|
|
|
@v1.route("/users")
|
|
def list_users(req, resp):
|
|
resp.media = []
|
|
|
|
@v1.route("/users/{user_id:int}")
|
|
def get_user(req, resp, *, user_id):
|
|
resp.media = {"id": user_id}
|
|
|
|
v2 = api.group("/v2")
|
|
|
|
@v2.route("/users")
|
|
def list_users_v2(req, resp):
|
|
resp.media = {"users": [], "total": 0}
|
|
|
|
This keeps your code organized without affecting the routing logic.
|
|
|
|
|
|
Mounting Other Apps
|
|
-------------------
|
|
|
|
Responder can mount any WSGI or ASGI application at a subroute. This is
|
|
incredibly useful for gradual migrations — you can run Flask and Responder
|
|
side by side, moving routes over one at a time::
|
|
|
|
from flask import Flask
|
|
|
|
flask_app = Flask(__name__)
|
|
|
|
@flask_app.route("/")
|
|
def hello():
|
|
return "Hello from Flask!"
|
|
|
|
api.mount("/flask", flask_app)
|
|
|
|
Requests to ``/flask/`` will be handled by Flask. Everything else goes
|
|
through Responder. Both WSGI and ASGI apps are supported — Responder
|
|
wraps WSGI apps in an ASGI adapter automatically.
|
|
|
|
You can also mount `marimo <https://marimo.io/>`_ notebooks as
|
|
interactive dashboards within your API::
|
|
|
|
import marimo
|
|
|
|
server = (
|
|
marimo.create_asgi_app()
|
|
.with_app(path="", root="./notebooks/dashboard.py")
|
|
.with_app(path="/analysis", root="./notebooks/analysis.py")
|
|
)
|
|
|
|
api.mount("/notebooks", server.build())
|
|
|
|
Notebooks are served at ``/notebooks/`` and ``/notebooks/analysis``,
|
|
with full interactivity — reactive cells, widgets, plots, and all.
|
|
|
|
|
|
Cookies
|
|
-------
|
|
|
|
`Cookies <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies>`_ are
|
|
small pieces of data that the server asks the browser to store and send
|
|
back with every subsequent request. They're the foundation of sessions,
|
|
authentication tokens, and user preferences on the web.
|
|
|
|
Reading and writing cookies is straightforward::
|
|
|
|
# Read cookies from the request
|
|
session_id = req.cookies.get("session_id")
|
|
|
|
# Set a cookie on the response
|
|
resp.cookies["hello"] = "world"
|
|
|
|
For production use, you'll want to set security directives. The
|
|
``httponly`` flag prevents JavaScript from reading the cookie (defending
|
|
against XSS attacks), and ``secure`` ensures it's only sent over HTTPS::
|
|
|
|
resp.set_cookie(
|
|
"token",
|
|
value="abc123",
|
|
max_age=3600, # expires in 1 hour
|
|
secure=True, # HTTPS only
|
|
httponly=True, # no JavaScript access
|
|
path="/",
|
|
)
|
|
|
|
|
|
Cookie-Based Sessions
|
|
---------------------
|
|
|
|
Sessions let you store per-user data across multiple requests. Responder's
|
|
built-in sessions are cookie-based — the session data is serialized, signed
|
|
with your secret key, and stored in a cookie. The signature prevents
|
|
tampering: if someone modifies the cookie, the signature won't match and
|
|
the data will be rejected::
|
|
|
|
@api.route("/login")
|
|
def login(req, resp):
|
|
resp.session["username"] = "alice"
|
|
|
|
@api.route("/profile")
|
|
def profile(req, resp):
|
|
resp.media = {"user": req.session.get("username")}
|
|
|
|
.. warning::
|
|
|
|
Always set a secret key in production. The default key is not secret::
|
|
|
|
api = responder.API(secret_key="your-secret-key-here")
|
|
|
|
|
|
Static Files
|
|
------------
|
|
|
|
Most web applications serve static assets — CSS stylesheets, JavaScript
|
|
files, images, fonts. Responder serves these from the ``static/`` directory
|
|
by default::
|
|
|
|
api = responder.API(static_dir="static", static_route="/static")
|
|
|
|
Place your assets in the ``static/`` directory and they'll be served
|
|
automatically at ``/static/style.css``, ``/static/app.js``, etc.
|
|
|
|
For single-page applications (React, Vue, Angular), you can serve
|
|
``index.html`` as the default response for all unmatched routes::
|
|
|
|
api.add_route("/", static=True)
|
|
|
|
|
|
CORS
|
|
----
|
|
|
|
`CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_ (Cross-
|
|
Origin Resource Sharing) is a security mechanism that controls which
|
|
websites can make requests to your API. Browsers enforce this — if your
|
|
API is at ``api.example.com`` and your frontend is at ``app.example.com``,
|
|
the browser will block requests unless your API explicitly allows it.
|
|
|
|
Enable CORS and configure which origins are allowed::
|
|
|
|
api = responder.API(cors=True, cors_params={
|
|
"allow_origins": ["https://app.example.com"],
|
|
"allow_methods": ["GET", "POST"],
|
|
"allow_headers": ["*"],
|
|
"allow_credentials": True,
|
|
"max_age": 600,
|
|
})
|
|
|
|
The default policy is restrictive — you must explicitly allow each origin.
|
|
Using ``["*"]`` for allow_origins permits any website to call your API,
|
|
which is fine for public APIs but not for private ones.
|
|
|
|
|
|
HSTS
|
|
----
|
|
|
|
`HSTS <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security>`_
|
|
(HTTP Strict Transport Security) tells browsers to always use HTTPS when
|
|
communicating with your server. Once a browser sees the HSTS header, it
|
|
will refuse to connect over plain HTTP, even if the user types ``http://``
|
|
in the address bar::
|
|
|
|
api = responder.API(enable_hsts=True)
|
|
|
|
|
|
Trusted Hosts
|
|
-------------
|
|
|
|
The ``Host`` header in an HTTP request tells the server which domain name
|
|
the client used. Attackers can forge this header to trick your application
|
|
into generating URLs to malicious domains (a class of attack called *Host
|
|
header injection*).
|
|
|
|
Restrict which hostnames your application accepts::
|
|
|
|
api = responder.API(allowed_hosts=["example.com", "*.example.com"])
|
|
|
|
Requests with unrecognized hosts get a ``400 Bad Request``. Wildcard
|
|
patterns are supported. By default, all hostnames are allowed.
|
|
|
|
|
|
Request ID
|
|
----------
|
|
|
|
In distributed systems, tracing a single request across multiple services
|
|
is essential for debugging. Request IDs are unique identifiers attached to
|
|
each request — if something goes wrong, you can search your logs for that
|
|
ID and find every related event.
|
|
|
|
Responder can auto-generate request IDs. If the client sends an
|
|
``X-Request-ID`` header (common in microservice architectures), it's
|
|
forwarded. Otherwise, a new UUID is generated::
|
|
|
|
api = responder.API(request_id=True)
|
|
|
|
The ID appears in the ``X-Request-ID`` response header.
|
|
|
|
|
|
Rate Limiting
|
|
-------------
|
|
|
|
Rate limiting prevents individual clients from overwhelming your API with
|
|
too many requests. It's essential for public APIs, and good practice even
|
|
for internal ones.
|
|
|
|
Responder includes a built-in token bucket rate limiter::
|
|
|
|
from responder.ext.ratelimit import RateLimiter
|
|
|
|
limiter = RateLimiter(requests=100, period=60) # 100 req/min
|
|
limiter.install(api)
|
|
|
|
When the limit is exceeded, clients receive a ``429 Too Many Requests``
|
|
response with a ``Retry-After`` header. Every response includes
|
|
``X-RateLimit-Limit`` and ``X-RateLimit-Remaining`` headers so clients
|
|
can pace themselves.
|
|
|
|
The rate limiter is per-client, keyed by IP address.
|
|
|
|
|
|
Structured Logging
|
|
------------------
|
|
|
|
Production applications need structured, searchable logs. Responder
|
|
includes built-in logging that automatically attaches request context
|
|
— request ID, HTTP method, path, and client IP — to every log message
|
|
emitted during request handling::
|
|
|
|
api = responder.API(enable_logging=True)
|
|
|
|
This gives you:
|
|
|
|
- **Access logging** with timing for every request::
|
|
|
|
2026-03-24 12:00:00 [INFO] responder.access — GET /users → 200 (1.2ms)
|
|
|
|
- **A logger on the API instance** — use ``api.log`` anywhere in
|
|
your routes. Request context (ID, method, path, client IP) is
|
|
attached automatically::
|
|
|
|
@api.route("/users/{user_id:int}")
|
|
def get_user(req, resp, *, user_id):
|
|
api.log.info("fetching user %d", user_id)
|
|
# => [INFO] responder.app -- fetching user 42 [GET /users/42] [req:a1b2c3] [client:10.0.0.1]
|
|
resp.media = {"id": user_id}
|
|
|
|
- **Request IDs** generated automatically (or forwarded from the
|
|
``X-Request-ID`` header) and included in responses.
|
|
|
|
The logging uses Python's standard ``logging`` module, so it works with
|
|
any handler — files, syslog, JSON formatters, Datadog, Sentry, whatever
|
|
you already use.
|
|
|
|
For additional loggers (e.g. in helper modules), use ``get_logger``::
|
|
|
|
from responder.ext.logging import get_logger
|
|
logger = get_logger("myapp.db")
|
|
|
|
You can also access the current request context directly::
|
|
|
|
from responder.ext.logging import RequestContext
|
|
|
|
@api.route("/debug")
|
|
def debug(req, resp):
|
|
resp.media = {
|
|
"request_id": RequestContext.get_request_id(),
|
|
"client_ip": RequestContext.get_client_ip(),
|
|
}
|
|
|
|
When ``enable_logging=True`` is set, it supersedes ``request_id=True``
|
|
— the logging middleware handles request IDs itself, so you don't get
|
|
duplicate headers.
|
|
|
|
|
|
Pydantic Validation
|
|
-------------------
|
|
|
|
`Pydantic <https://docs.pydantic.dev/>`_ models integrate directly with
|
|
Responder's routing. Set ``request_model`` to validate incoming data and
|
|
``response_model`` to control the shape of outgoing data::
|
|
|
|
from pydantic import BaseModel
|
|
|
|
class ItemIn(BaseModel):
|
|
name: str
|
|
price: float
|
|
|
|
class ItemOut(BaseModel):
|
|
id: int
|
|
name: str
|
|
price: float
|
|
|
|
@api.route("/items", methods=["POST"],
|
|
request_model=ItemIn, response_model=ItemOut)
|
|
async def create_item(req, resp):
|
|
data = await req.media()
|
|
resp.media = {"id": 1, **data}
|
|
|
|
When ``request_model`` is set:
|
|
|
|
- Valid requests are parsed and the data is available via ``await req.media()``
|
|
- Invalid requests get an automatic ``422 Unprocessable Entity`` response
|
|
with detailed error messages — you don't write any validation code
|
|
|
|
When ``response_model`` is set:
|
|
|
|
- The response is serialized through the model before being sent
|
|
- Extra fields are stripped automatically
|
|
- Type coercion happens at the boundary
|
|
|
|
This is the recommended way to build validated REST APIs with Responder.
|
|
See the :doc:`tutorial-rest` for a complete walkthrough.
|
|
|
|
|
|
Content Negotiation
|
|
-------------------
|
|
|
|
Responder automatically negotiates the response format based on the
|
|
client's ``Accept`` header. Set ``resp.media`` to a Python object and
|
|
the right thing happens:
|
|
|
|
- ``Accept: application/json`` (default) → JSON
|
|
- ``Accept: application/x-yaml`` → YAML
|
|
- ``Accept: application/x-msgpack`` → MessagePack
|
|
|
|
This means a single endpoint serves multiple formats without any
|
|
conditional logic in your code::
|
|
|
|
@api.route("/data")
|
|
def data(req, resp):
|
|
resp.media = {"key": "value"}
|
|
|
|
Clients get the format they ask for::
|
|
|
|
$ curl http://localhost:5042/data
|
|
{"key": "value"}
|
|
|
|
$ curl -H "Accept: application/x-yaml" http://localhost:5042/data
|
|
key: value
|
|
|
|
|
|
MessagePack
|
|
-----------
|
|
|
|
`MessagePack <https://msgpack.org/>`_ is a binary serialization format
|
|
that's more compact and faster to parse than JSON. It's useful for
|
|
high-throughput APIs, IoT devices, and anywhere bandwidth matters.
|
|
|
|
Responder supports MessagePack alongside JSON and YAML::
|
|
|
|
# Decode a MessagePack request body
|
|
data = await req.media("msgpack")
|
|
|
|
# Respond with MessagePack
|
|
resp.media = {"result": [1, 2, 3]}
|
|
|
|
Content negotiation works automatically — clients can send
|
|
``Accept: application/x-msgpack`` to receive MessagePack responses
|
|
instead of JSON. You can also explicitly decode MessagePack request
|
|
bodies by passing ``"msgpack"`` to ``req.media()``.
|