Files
responder/docs/source/tutorial-middleware.rst
kennethreitzandClaude Fable 5 428181a096 Add JWT/OAuth2 auth, Problem exception, function middleware, and a broad feature sweep for 8.1.0
Flagship: JWTAuth (HS256/RS256/ES256, exp/aud/iss, JWKS with off-loop
fetch, scope claims -> requires()) and OAuth2Auth flow descriptions that
light up Swagger UI's Authorize button, behind a new responder[jwt] extra.

Also additive: responder.Problem (raisable RFC 9457) + RFC 9457 abort();
@api.middleware function-style HTTP middleware; Pydantic bodies and
Query/Header/Cookie/Path markers on class-based and WebSocket handlers;
Form-model binding; write-side preconditions (If-Match/If-Unmodified-Since
-> 412); q-ranked content negotiation; get_list for repeated headers;
status_code= route option; OpenAPI document caching; rate-limiter key=/
fail-open/X-RateLimit-Reset; configurable metrics buckets + in-flight
gauge; RFC 8288 pagination Link headers; Redis session serializer;
unified request-ID policy; CLI --host/--port/--server/--reload; optional
orjson encode backend; AsyncTestClient + SSE test helpers; fakeredis
backend coverage. Five DeprecationWarnings start the 9.0 clock.

Every feature was implemented test-first, then the combined diff went
through a 64-agent adversarial review that caught 19 issues including two
criticals (an auth scope-claim regression that flipped authorization
decisions, and a sync-middleware threadpool deadlock) -- all fixed with
regression tests before release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:08:42 -04:00

253 lines
9.5 KiB
ReStructuredText

Writing Middleware
==================
Middleware sits between the server and your route handlers, processing
every request and response that flows through your application. It's the
right tool for cross-cutting concerns — things that apply to *all*
requests, not just specific routes.
Common middleware use cases:
- Request logging and timing
- Authentication and authorization
- Adding security headers
- Request ID generation
- Rate limiting
- Response compression (built-in)
Hooks vs. Middleware
--------------------
Responder gives you two levels of request processing:
**Hooks** (``before_request`` / ``after_request``) run inside Responder's
routing layer. They receive Responder's ``req`` and ``resp`` objects and
are the simplest way to add behavior::
@api.route(before_request=True)
def add_header(req, resp):
resp.headers["X-Powered-By"] = "Responder"
@api.after_request()
def log_request(req, resp):
print(f"{req.method} {req.url.path} -> {resp.status_code}")
.. note::
``req.method`` is uppercase (``"GET"``, ``"POST"``, …) and compares
case-sensitively, so compare against uppercase strings: ``req.method == "GET"``.
**Middleware** runs at the ASGI level, wrapping the entire application.
It's more powerful but more complex — you work with raw ASGI scopes
instead of Responder objects. Use middleware when you need to process
requests *before* they reach Responder's routing, or when you need to
integrate with Starlette middleware.
Function Middleware
-------------------
The quickest way to write HTTP middleware is the ``@api.middleware("http")``
decorator — a plain function that receives the Starlette ``Request`` and a
``call_next`` callable, no class boilerplate required::
import time
@api.middleware("http")
async def add_timing(request, call_next):
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
response.headers["X-Response-Time"] = f"{elapsed:.4f}s"
return response
Call ``call_next(request)`` to pass the request onward; the return value is a
Starlette ``Response`` you can modify (or replace — return your own response
without calling ``call_next`` to short-circuit).
Synchronous functions work too. They are offloaded to the threadpool like
sync views, and the ``call_next`` they receive is a plain blocking callable::
@api.middleware("http")
def tag_response(request, call_next):
response = call_next(request)
response.headers["X-Tag"] = "handled"
return response
Function middleware registers through the same stack as ``add_middleware``,
so the two compose freely — see `Middleware Order`_ for where it sits.
Non-HTTP traffic (WebSockets, lifespan) passes through untouched.
Using Starlette Middleware
--------------------------
Responder is built on Starlette, so any Starlette middleware works
out of the box::
from starlette.middleware.base import BaseHTTPMiddleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
import time
start = time.time()
response = await call_next(request)
duration = time.time() - start
response.headers["X-Response-Time"] = f"{duration:.3f}s"
return response
api.add_middleware(TimingMiddleware)
The ``dispatch`` method receives a Starlette ``Request`` and a
``call_next`` function. Call ``call_next(request)`` to pass the request
to the next middleware (or to your route handler). The return value is
a Starlette ``Response`` that you can modify before it's sent.
Built-in Middleware
-------------------
Every Responder app ships with a small stack wired up for you:
- **ServerErrorMiddleware** — catches unhandled exceptions and renders a 500
- **ExceptionMiddleware** — routes ``HTTPException``\ s and status codes to your handlers
- **TrustedHostMiddleware** — validates the ``Host`` header (``["*"]`` by default)
- **GZipMiddleware** — compresses responses larger than 500 bytes (on by default)
A few more are wired in on demand, by constructor flag:
- **SessionMiddleware** — signed cookie sessions, on unless you pass
``sessions=False``. Secure by default: the signing key never falls back to a
public default, cookies are ``Secure`` in production, and ``req.session`` /
``resp.session`` raise ``RuntimeError`` when sessions are off. See
:doc:`guide-config` for the full story.
- **CORSMiddleware** — ``responder.API(cors=True)``
- **HTTPSRedirectMiddleware** — ``responder.API(enable_hsts=True)``
- **RequestIDMiddleware** — ``responder.API(request_id=True)`` adds an
``X-Request-ID`` header to every response
- **LoggingMiddleware** — ``responder.API(enable_logging=True)`` for structured
per-request logging (it handles request IDs itself, superseding ``request_id``)
- **MetricsMiddleware** — ``responder.API(metrics_route="/metrics")`` exposes
Prometheus metrics
The observability options (request IDs, logging, metrics) are covered in
:doc:`tour`.
Adding Third-Party Middleware
-----------------------------
Any ASGI middleware can be added with ``api.add_middleware()``::
from some_package import SomeMiddleware
api.add_middleware(SomeMiddleware, option1="value", option2=True)
Keyword arguments are passed to the middleware's constructor.
Middleware can be registered any time before the first request, not only at
construction — the ASGI stack is assembled lazily and rebuilt whenever you add
more. That assembled stack is exposed as the read-only ``api.app`` property, so
you can't inject middleware by assigning to it. Use ``api.add_middleware()``, or
wrap the API object itself for a truly outermost layer (see `Middleware Order`_).
Middleware Order
----------------
Middleware wraps your application like the layers of an onion. A request
travels inward through every layer to your route, and the response travels
back outward in reverse.
The full built-in stack, from outermost to innermost, is:
1. **LoggingMiddleware** (``enable_logging=True``) *or* **RequestIDMiddleware**
(``request_id=True``) — the observability tier. It wraps everything below,
so even a rendered 500 carries its ``X-Request-ID`` and real status.
2. **MetricsMiddleware** (``metrics_route=...``)
3. **ServerErrorMiddleware** — the outermost *application* layer; it catches
errors from every middleware and route beneath it.
4. **your middleware** (added with ``add_middleware`` or ``@api.middleware``)
5. **TrustedHostMiddleware**
6. **HTTPSRedirectMiddleware** (``enable_hsts=True``)
7. **CORSMiddleware** (``cors=True``)
8. **SessionMiddleware** (unless ``sessions=False``)
9. **GZipMiddleware** (on by default)
10. **ExceptionMiddleware** — routes non-500 exceptions to your handlers
11. **your routes**
Two consequences worth knowing:
- Your middleware sits *inside* ``ServerErrorMiddleware``, so an exception it
raises is caught and rendered as a 500 instead of crashing the server.
- Sessions sit beneath ``ServerErrorMiddleware``, so they are *not* persisted on
an unhandled 500.
``api.add_middleware()`` inserts your middleware just inside
``ServerErrorMiddleware`` — *not* at the very top of the stack. Among your own
middleware — function-style and class-based alike — the most-recently-added is
the outermost and runs first, so if middleware A depends on B having run
first, add B before A.
To wrap *everything* — including error rendering and the observability tier —
wrap the API object itself::
asgi = MyOutermostMiddleware(api)
That ``asgi`` callable is what you then serve.
Writing Pure ASGI Middleware
----------------------------
For maximum performance and control, you can write middleware as a plain
ASGI application. This bypasses Starlette's ``BaseHTTPMiddleware``
abstraction — it's faster and gives you direct access to the ASGI
protocol::
class SecurityHeadersMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def send_with_headers(message):
if message["type"] == "http.response.start":
extra = [
(b"x-content-type-options", b"nosniff"),
(b"x-frame-options", b"DENY"),
(b"referrer-policy", b"strict-origin-when-cross-origin"),
]
message["headers"] = list(message["headers"]) + extra
await send(message)
await self.app(scope, receive, send_with_headers)
api.add_middleware(SecurityHeadersMiddleware)
This is the same pattern used internally by Starlette and uvicorn. The
middleware receives the ASGI ``scope``, ``receive``, and ``send`` callables,
and wraps ``send`` to inject headers into the response.
For most cases, ``BaseHTTPMiddleware`` is simpler and perfectly fine.
Use the pure ASGI approach when you need to handle WebSocket connections,
streaming responses, or want to avoid the overhead of request/response
object creation.
When to Use What
-----------------
- **Simple header additions, logging, auth checks** → use hooks
- **Response transformation, timing, third-party integrations** → use
middleware (start with ``@api.middleware("http")``)
- **Rate limiting** → use the built-in ``RateLimiter`` (it uses hooks internally)
- **Request ID** → use ``api = responder.API(request_id=True)``
Start with hooks. They're simpler and cover most cases. Graduate to
middleware when hooks aren't enough.