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}") **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. 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 ------------------- Responder configures several middleware components automatically: - **GZipMiddleware** — compresses responses larger than 500 bytes - **TrustedHostMiddleware** — validates the ``Host`` header - **ServerErrorMiddleware** — catches unhandled exceptions - **ExceptionMiddleware** — routes exceptions to your handlers - **SessionMiddleware** — manages signed cookie sessions Optional middleware you can enable: - **CORSMiddleware** — ``api = responder.API(cors=True)`` - **HTTPSRedirectMiddleware** — ``api = responder.API(enable_hsts=True)`` 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 Order ---------------- Middleware wraps your application like layers of an onion. The *last* middleware added is the *outermost* layer — it sees the request first and the response last. Responder's built-in middleware stack (from outermost to innermost): 1. SessionMiddleware 2. ServerErrorMiddleware 3. CORSMiddleware (if enabled) 4. TrustedHostMiddleware 5. HTTPSRedirectMiddleware (if enabled) 6. GZipMiddleware 7. ExceptionMiddleware 8. Your routes When you call ``api.add_middleware()``, your middleware is added *outside* the existing stack. Keep this in mind for ordering dependencies — if middleware A depends on middleware B having run first, add B before A. When to Use What ----------------- - **Simple header additions, logging, auth checks** → use hooks - **Response transformation, timing, third-party integrations** → use middleware - **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.