This commit is contained in:
kennethreitz
2026-03-24 19:52:20 +00:00
parent 8442a0af85
commit 60b4516894
26 changed files with 741 additions and 26 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+87 -4
View File
@@ -12,6 +12,20 @@ The central object of every Responder application. It holds your routes,
middleware, templates, and configuration. Create one at the top of your
module and use it to define your entire web service.
Quick example::
import responder
api = responder.API(
title="My Service", # OpenAPI title
version="1.0", # OpenAPI version
openapi="3.0.2", # enable OpenAPI
docs_route="/docs", # Swagger UI at /docs
cors=True, # enable CORS
secret_key="change-me", # session signing key
allowed_hosts=["example.com"],
)
.. module:: responder
.. autoclass:: API
@@ -28,6 +42,27 @@ parameters, the request body, cookies, and more.
Most properties are synchronous, but reading the body requires ``await``
because it involves I/O.
Common patterns::
# Headers (case-insensitive)
token = req.headers.get("Authorization")
# Query parameters: /search?q=python&page=2
query = req.params["q"]
# JSON body
data = await req.media()
# Form data
form = await req.media("form")
# File uploads
files = await req.media("files")
# Client info
ip, port = req.client
is_https = req.is_secure
.. autoclass:: Request
:inherited-members:
@@ -39,6 +74,19 @@ The response object is passed into every view as the second argument.
Mutate it to control what gets sent back to the client — the body,
status code, headers, and cookies.
Common patterns::
resp.text = "plain text" # text/plain
resp.html = "<h1>Hello</h1>" # text/html
resp.media = {"key": "value"} # application/json
resp.content = b"raw bytes" # application/octet-stream
resp.file("path/to/file.pdf") # auto content-type
resp.stream_file("large/export.csv") # streamed
resp.status_code = 201
resp.headers["X-Custom"] = "value"
resp.cookies["session"] = "abc123"
.. autoclass:: Response
:inherited-members:
@@ -47,7 +95,13 @@ Route Groups
------------
Group related routes under a shared URL prefix — useful for API versioning
and organizing large applications.
and organizing large applications::
v1 = api.group("/v1")
@v1.route("/users")
def list_users(req, resp):
resp.media = []
.. autoclass:: responder.api.RouteGroup
:members:
@@ -57,7 +111,19 @@ Background Queue
----------------
Run tasks in background threads without blocking the response. Available
as ``api.background``.
as ``api.background``::
@api.route("/submit")
async def submit(req, resp):
data = await req.media()
@api.background.task
def process(data):
# runs in a thread pool
...
process(data)
resp.media = {"status": "accepted"}
.. autoclass:: responder.background.BackgroundQueue
:members:
@@ -67,6 +133,8 @@ Query Dict
----------
A dictionary subclass for query string parameters with multi-value support.
Behaves like a normal dict for single values, but supports ``getlist()``
for parameters that appear multiple times (e.g. ``?tag=a&tag=b``).
.. autoclass:: responder.models.QueryDict
:members:
@@ -76,7 +144,15 @@ Rate Limiter
------------
In-memory token bucket rate limiter. Limits requests per client IP address
and returns ``429 Too Many Requests`` when exceeded.
and returns ``429 Too Many Requests`` when exceeded::
from responder.ext.ratelimit import RateLimiter
limiter = RateLimiter(requests=100, period=60) # 100 req/min
limiter.install(api)
Response headers: ``X-RateLimit-Limit``, ``X-RateLimit-Remaining``,
and ``Retry-After`` (when limited).
.. autoclass:: responder.ext.ratelimit.RateLimiter
:members:
@@ -86,7 +162,14 @@ Status Code Helpers
-------------------
Convenience functions for checking which category a status code falls
into. Useful in middleware and after-request hooks.
into. Useful in middleware and after-request hooks::
from responder.status_codes import is_200, is_400, is_500
@api.after_request()
def log_errors(req, resp):
if is_400(resp.status_code) or is_500(resp.status_code):
print(f"Error: {req.method} {req.url.path} -> {resp.status_code}")
.. autofunction:: responder.status_codes.is_100
+5 -4
View File
@@ -1,7 +1,8 @@
# Backlog
## Future Ideas
- Consider adding `after_request` hooks (complement to `before_request`)
- Explore WebSocket before_request short-circuit support
- Add rate limiting middleware
- Consider async template rendering by default
- WebSocket before_request short-circuit support (reject before accept)
- Per-route rate limiting (different limits for different endpoints)
- Built-in structured logging with request context
- OpenAPI 3.1 support
- HTTP/2 server push
+19
View File
@@ -71,6 +71,25 @@ For URLs, use a fragment::
$ responder run https://example.com/app.py#service
Environment Variables
---------------------
Responder automatically reads the ``PORT`` environment variable at
runtime:
- ``PORT`` — bind to ``0.0.0.0`` on this port (cloud platform convention)
When ``PORT`` is set, the server binds to all interfaces automatically.
This is how cloud platforms like Fly.io, Railway, and Heroku inject the
listen port.
For other settings like ``SECRET_KEY``, read them in your application
code and pass them to ``responder.API()``::
import os
api = responder.API(secret_key=os.environ["SECRET_KEY"])
Building Frontend Assets
-------------------------
+82
View File
@@ -67,6 +67,27 @@ The pattern is always the same: deploy your code, set the start command
to ``python api.py``, and the platform handles the rest.
Health Check Endpoint
---------------------
Every production deployment needs a health check — a lightweight endpoint
that monitoring tools, load balancers, and orchestrators can poll to verify
your service is running::
@api.route("/health")
def health(req, resp):
resp.media = {"status": "healthy"}
Keep it simple. Don't query the database or do expensive work — the health
check should return instantly. Cloud platforms, Docker, and Kubernetes all
look for an HTTP 200 to confirm your service is alive.
For Docker, add a ``HEALTHCHECK`` instruction::
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
Uvicorn Directly
----------------
@@ -82,6 +103,45 @@ Uvicorn supports many options — SSL certificates, access logging, graceful
shutdown timeouts, and more. See the
`uvicorn documentation <https://www.uvicorn.org/deployment/>`_ for details.
For platforms like Heroku or Railway that use a ``Procfile``::
web: uvicorn api:api --host 0.0.0.0 --port $PORT --workers 4
Docker Compose
--------------
For local development with databases and other services, Docker Compose
ties everything together::
# docker-compose.yml
services:
api:
build: .
ports:
- "5042:80"
environment:
- PORT=80
- DATABASE_URL=postgresql+asyncpg://user:pass@db/myapp
- SECRET_KEY=dev-secret
depends_on:
- db
db:
image: docker.io/postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Run with ``docker compose up``. The API waits for ``db`` to start, then
connects using the ``DATABASE_URL`` environment variable.
Reverse Proxy
-------------
@@ -95,9 +155,31 @@ front of your application for:
- **Static asset serving** — offload static files to the proxy
- **Rate limiting** — at the infrastructure level
A minimal Caddy config that handles HTTPS automatically::
# Caddyfile
example.com {
reverse_proxy localhost:5042
}
Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work
correctly behind proxies that set standard forwarding headers
(``X-Forwarded-For``, ``X-Forwarded-Proto``).
That said, uvicorn is production-ready on its own. Many applications run
uvicorn directly without a reverse proxy and do just fine.
Production Checklist
--------------------
Before going live:
- **Set a secret key** — ``SECRET_KEY`` env var, never the default
- **Disable debug mode** — ``DEBUG=false`` or omit it entirely
- **Set allowed hosts** — restrict to your actual domain names
- **Use multiple workers** — ``--workers 4`` or more, depending on CPU cores
- **Add a health check** — ``/health`` endpoint for monitoring
- **Enable HTTPS** — via your proxy, cloud platform, or uvicorn's ``--ssl-*`` flags
- **Set up logging** — uvicorn logs requests by default; pipe them to your log aggregator
- **Pin your dependencies** — commit ``uv.lock`` for reproducible deploys
+6 -2
View File
@@ -59,21 +59,25 @@ What You Get
One ``pip install``, batteries included:
- Pydantic request validation and response serialization.
- Mount Flask, Django, or any WSGI/ASGI app at a subroute.
- Gzip compression, HSTS, CORS, and trusted host validation.
- Before-request hooks that can short-circuit for auth guards.
- Before-request and after-request hooks for auth and logging.
- A test client for fast, in-process testing with pytest.
- Route parameters with f-string syntax and type convertors.
- Lifespan context managers for startup and shutdown logic.
- Custom exception handlers for clean error responses.
- `GraphQL`_ with Graphene and a built-in GraphiQL IDE.
- Server-Sent Events for real-time streaming.
- File serving with automatic content-type detection.
- Sync and async views — ``async`` is always optional.
- Class-based views with ``on_get``, ``on_post``, ``on_request``.
- Built-in rate limiting with ``X-RateLimit`` headers.
- Content negotiation: JSON, YAML, and MessagePack.
- A pleasant API with a single import statement.
- OpenAPI schema generation with Swagger UI.
- A production `uvicorn`_ server, ready to deploy.
- HTTP method filtering for REST APIs.
- Route groups for API versioning.
- Signed cookie-based sessions.
- Background tasks in a thread pool.
- WebSocket support.
+54
View File
@@ -269,6 +269,57 @@ just like in production. You can verify their effects on the response::
assert r.headers["X-Served-By"] == "responder"
Testing Rate Limiting
---------------------
Rate limiters are just hooks — they run automatically during tests.
Verify the headers and the 429 response::
from responder.ext.ratelimit import RateLimiter
def test_rate_limiting():
api = responder.API(allowed_hosts=["localhost"])
limiter = RateLimiter(requests=2, period=60)
limiter.install(api)
@api.route("/")
def view(req, resp):
resp.text = "ok"
# First two requests succeed
for _ in range(2):
r = api.requests.get("http://localhost/")
assert r.status_code == 200
assert "X-RateLimit-Remaining" in r.headers
# Third request is rate limited
r = api.requests.get("http://localhost/")
assert r.status_code == 429
Testing Mounted Apps
--------------------
When testing WSGI apps mounted at a subroute, use ``localhost`` as the
host to avoid Werkzeug's trusted host validation::
from flask import Flask
def test_flask_mount():
api = responder.API(allowed_hosts=["localhost"])
flask_app = Flask(__name__)
@flask_app.route("/")
def hello():
return "Hello from Flask!"
api.mount("/flask", flask_app)
r = api.requests.get("http://localhost/flask")
assert r.status_code == 200
assert "Hello from Flask" in r.text
Tips
----
@@ -284,3 +335,6 @@ Tips
- **Test the contract, not the implementation.** Assert on status codes,
response bodies, and headers — not on internal state.
- **Use ``localhost`` for mounted WSGI apps.** Werkzeug 3.1.7+ validates
the ``Host`` header, so avoid synthetic hosts like ``;`` in tests.
+73 -2
View File
@@ -596,6 +596,73 @@ can pace themselves.
The rate limiter is per-client, keyed by IP address.
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
-----------
@@ -608,6 +675,10 @@ Responder supports MessagePack alongside JSON and YAML::
# Decode a MessagePack request body
data = await req.media("msgpack")
Content negotiation works too — clients can send
# 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.
instead of JSON. You can also explicitly decode MessagePack request
bodies by passing ``"msgpack"`` to ``req.media()``.
+41
View File
@@ -117,6 +117,47 @@ the existing stack. Keep this in mind for ordering dependencies — if
middleware A depends on middleware B having run first, add B before A.
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
-----------------
+91 -5
View File
File diff suppressed because one or more lines are too long
+5 -4
View File
@@ -44,10 +44,11 @@
<section id="future-ideas">
<h2>Future Ideas<a class="headerlink" href="#future-ideas" title="Link to this heading"></a></h2>
<ul class="simple">
<li><p>Consider adding <code class="docutils literal notranslate"><span class="pre">after_request</span></code> hooks (complement to <code class="docutils literal notranslate"><span class="pre">before_request</span></code>)</p></li>
<li><p>Explore WebSocket before_request short-circuit support</p></li>
<li><p>Add rate limiting middleware</p></li>
<li><p>Consider async template rendering by default</p></li>
<li><p>WebSocket before_request short-circuit support (reject before accept)</p></li>
<li><p>Per-route rate limiting (different limits for different endpoints)</p></li>
<li><p>Built-in structured logging with request context</p></li>
<li><p>OpenAPI 3.1 support</p></li>
<li><p>HTTP/2 server push</p></li>
</ul>
</section>
</section>
+18
View File
@@ -104,6 +104,23 @@ $ responder run myapp.py:application
</pre></div>
</div>
</section>
<section id="environment-variables">
<h2>Environment Variables<a class="headerlink" href="#environment-variables" title="Link to this heading"></a></h2>
<p>Responder automatically reads the <code class="docutils literal notranslate"><span class="pre">PORT</span></code> environment variable at
runtime:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">PORT</span></code> — bind to <code class="docutils literal notranslate"><span class="pre">0.0.0.0</span></code> on this port (cloud platform convention)</p></li>
</ul>
<p>When <code class="docutils literal notranslate"><span class="pre">PORT</span></code> is set, the server binds to all interfaces automatically.
This is how cloud platforms like Fly.io, Railway, and Heroku inject the
listen port.</p>
<p>For other settings like <code class="docutils literal notranslate"><span class="pre">SECRET_KEY</span></code>, read them in your application
code and pass them to <code class="docutils literal notranslate"><span class="pre">responder.API()</span></code>:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span><span class="w"> </span><span class="nn">os</span>
<span class="n">api</span> <span class="o">=</span> <span class="n">responder</span><span class="o">.</span><span class="n">API</span><span class="p">(</span><span class="n">secret_key</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&quot;SECRET_KEY&quot;</span><span class="p">])</span>
</pre></div>
</div>
</section>
<section id="building-frontend-assets">
<h2>Building Frontend Assets<a class="headerlink" href="#building-frontend-assets" title="Link to this heading"></a></h2>
<p>If your project includes a JavaScript frontend with a <code class="docutils literal notranslate"><span class="pre">package.json</span></code>,
@@ -145,6 +162,7 @@ $ responder build /path/to/frontend
<li><a class="reference internal" href="#launching-from-a-file">Launching from a File</a></li>
<li><a class="reference internal" href="#launching-from-a-url">Launching from a URL</a></li>
<li><a class="reference internal" href="#custom-instance-names">Custom Instance Names</a></li>
<li><a class="reference internal" href="#environment-variables">Environment Variables</a></li>
<li><a class="reference internal" href="#building-frontend-assets">Building Frontend Assets</a></li>
</ul>
</li>
+80
View File
@@ -99,6 +99,25 @@ the convention that virtually every cloud platform uses.</p>
<p>The pattern is always the same: deploy your code, set the start command
to <code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">api.py</span></code>, and the platform handles the rest.</p>
</section>
<section id="health-check-endpoint">
<h2>Health Check Endpoint<a class="headerlink" href="#health-check-endpoint" title="Link to this heading"></a></h2>
<p>Every production deployment needs a health check — a lightweight endpoint
that monitoring tools, load balancers, and orchestrators can poll to verify
your service is running:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">&quot;/health&quot;</span><span class="p">)</span>
<span class="k">def</span><span class="w"> </span><span class="nf">health</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;healthy&quot;</span><span class="p">}</span>
</pre></div>
</div>
<p>Keep it simple. Dont query the database or do expensive work — the health
check should return instantly. Cloud platforms, Docker, and Kubernetes all
look for an HTTP 200 to confirm your service is alive.</p>
<p>For Docker, add a <code class="docutils literal notranslate"><span class="pre">HEALTHCHECK</span></code> instruction:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">HEALTHCHECK</span> <span class="o">--</span><span class="n">interval</span><span class="o">=</span><span class="mi">30</span><span class="n">s</span> <span class="o">--</span><span class="n">timeout</span><span class="o">=</span><span class="mi">3</span><span class="n">s</span> \
<span class="n">CMD</span> <span class="n">curl</span> <span class="o">-</span><span class="n">f</span> <span class="n">http</span><span class="p">:</span><span class="o">//</span><span class="n">localhost</span><span class="o">/</span><span class="n">health</span> <span class="o">||</span> <span class="n">exit</span> <span class="mi">1</span>
</pre></div>
</div>
</section>
<section id="uvicorn-directly">
<h2>Uvicorn Directly<a class="headerlink" href="#uvicorn-directly" title="Link to this heading"></a></h2>
<p>For production deployments where you want more control, bypass
@@ -111,6 +130,43 @@ independently. A good starting point is 2-4 workers per CPU core.</p>
<p>Uvicorn supports many options — SSL certificates, access logging, graceful
shutdown timeouts, and more. See the
<a class="reference external" href="https://www.uvicorn.org/deployment/">uvicorn documentation</a> for details.</p>
<p>For platforms like Heroku or Railway that use a <code class="docutils literal notranslate"><span class="pre">Procfile</span></code>:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>web: uvicorn api:api --host 0.0.0.0 --port $PORT --workers 4
</pre></div>
</div>
</section>
<section id="docker-compose">
<h2>Docker Compose<a class="headerlink" href="#docker-compose" title="Link to this heading"></a></h2>
<p>For local development with databases and other services, Docker Compose
ties everything together:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># docker-compose.yml</span>
<span class="n">services</span><span class="p">:</span>
<span class="n">api</span><span class="p">:</span>
<span class="n">build</span><span class="p">:</span> <span class="o">.</span>
<span class="n">ports</span><span class="p">:</span>
<span class="o">-</span> <span class="s2">&quot;5042:80&quot;</span>
<span class="n">environment</span><span class="p">:</span>
<span class="o">-</span> <span class="n">PORT</span><span class="o">=</span><span class="mi">80</span>
<span class="o">-</span> <span class="n">DATABASE_URL</span><span class="o">=</span><span class="n">postgresql</span><span class="o">+</span><span class="n">asyncpg</span><span class="p">:</span><span class="o">//</span><span class="n">user</span><span class="p">:</span><span class="k">pass</span><span class="nd">@db</span><span class="o">/</span><span class="n">myapp</span>
<span class="o">-</span> <span class="n">SECRET_KEY</span><span class="o">=</span><span class="n">dev</span><span class="o">-</span><span class="n">secret</span>
<span class="n">depends_on</span><span class="p">:</span>
<span class="o">-</span> <span class="n">db</span>
<span class="n">db</span><span class="p">:</span>
<span class="n">image</span><span class="p">:</span> <span class="n">docker</span><span class="o">.</span><span class="n">io</span><span class="o">/</span><span class="n">postgres</span><span class="p">:</span><span class="mi">16</span><span class="o">-</span><span class="n">alpine</span>
<span class="n">environment</span><span class="p">:</span>
<span class="n">POSTGRES_USER</span><span class="p">:</span> <span class="n">user</span>
<span class="n">POSTGRES_PASSWORD</span><span class="p">:</span> <span class="k">pass</span>
<span class="n">POSTGRES_DB</span><span class="p">:</span> <span class="n">myapp</span>
<span class="n">volumes</span><span class="p">:</span>
<span class="o">-</span> <span class="n">pgdata</span><span class="p">:</span><span class="o">/</span><span class="n">var</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">postgresql</span><span class="o">/</span><span class="n">data</span>
<span class="n">volumes</span><span class="p">:</span>
<span class="n">pgdata</span><span class="p">:</span>
</pre></div>
</div>
<p>Run with <code class="docutils literal notranslate"><span class="pre">docker</span> <span class="pre">compose</span> <span class="pre">up</span></code>. The API waits for <code class="docutils literal notranslate"><span class="pre">db</span></code> to start, then
connects using the <code class="docutils literal notranslate"><span class="pre">DATABASE_URL</span></code> environment variable.</p>
</section>
<section id="reverse-proxy">
<h2>Reverse Proxy<a class="headerlink" href="#reverse-proxy" title="Link to this heading"></a></h2>
@@ -123,12 +179,33 @@ front of your application for:</p>
<li><p><strong>Static asset serving</strong> — offload static files to the proxy</p></li>
<li><p><strong>Rate limiting</strong> — at the infrastructure level</p></li>
</ul>
<p>A minimal Caddy config that handles HTTPS automatically:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Caddyfile</span>
<span class="n">example</span><span class="o">.</span><span class="n">com</span> <span class="p">{</span>
<span class="n">reverse_proxy</span> <span class="n">localhost</span><span class="p">:</span><span class="mi">5042</span>
<span class="p">}</span>
</pre></div>
</div>
<p>Responders <code class="docutils literal notranslate"><span class="pre">TrustedHostMiddleware</span></code> and <code class="docutils literal notranslate"><span class="pre">HTTPSRedirectMiddleware</span></code> work
correctly behind proxies that set standard forwarding headers
(<code class="docutils literal notranslate"><span class="pre">X-Forwarded-For</span></code>, <code class="docutils literal notranslate"><span class="pre">X-Forwarded-Proto</span></code>).</p>
<p>That said, uvicorn is production-ready on its own. Many applications run
uvicorn directly without a reverse proxy and do just fine.</p>
</section>
<section id="production-checklist">
<h2>Production Checklist<a class="headerlink" href="#production-checklist" title="Link to this heading"></a></h2>
<p>Before going live:</p>
<ul class="simple">
<li><p><strong>Set a secret key</strong><code class="docutils literal notranslate"><span class="pre">SECRET_KEY</span></code> env var, never the default</p></li>
<li><p><strong>Disable debug mode</strong><code class="docutils literal notranslate"><span class="pre">DEBUG=false</span></code> or omit it entirely</p></li>
<li><p><strong>Set allowed hosts</strong> — restrict to your actual domain names</p></li>
<li><p><strong>Use multiple workers</strong><code class="docutils literal notranslate"><span class="pre">--workers</span> <span class="pre">4</span></code> or more, depending on CPU cores</p></li>
<li><p><strong>Add a health check</strong><code class="docutils literal notranslate"><span class="pre">/health</span></code> endpoint for monitoring</p></li>
<li><p><strong>Enable HTTPS</strong> — via your proxy, cloud platform, or uvicorns <code class="docutils literal notranslate"><span class="pre">--ssl-*</span></code> flags</p></li>
<li><p><strong>Set up logging</strong> — uvicorn logs requests by default; pipe them to your log aggregator</p></li>
<li><p><strong>Pin your dependencies</strong> — commit <code class="docutils literal notranslate"><span class="pre">uv.lock</span></code> for reproducible deploys</p></li>
</ul>
</section>
</section>
@@ -160,8 +237,11 @@ uvicorn directly without a reverse proxy and do just fine.</p>
<li><a class="reference internal" href="#running-locally">Running Locally</a></li>
<li><a class="reference internal" href="#docker">Docker</a></li>
<li><a class="reference internal" href="#cloud-platforms">Cloud Platforms</a></li>
<li><a class="reference internal" href="#health-check-endpoint">Health Check Endpoint</a></li>
<li><a class="reference internal" href="#uvicorn-directly">Uvicorn Directly</a></li>
<li><a class="reference internal" href="#docker-compose">Docker Compose</a></li>
<li><a class="reference internal" href="#reverse-proxy">Reverse Proxy</a></li>
<li><a class="reference internal" href="#production-checklist">Production Checklist</a></li>
</ul>
</li>
</ul>
+15 -2
View File
@@ -89,21 +89,25 @@ work with — welcome.</p>
<h2>What You Get<a class="headerlink" href="#what-you-get" title="Link to this heading"></a></h2>
<p>One <code class="docutils literal notranslate"><span class="pre">pip</span> <span class="pre">install</span></code>, batteries included:</p>
<ul class="simple">
<li><p>Pydantic request validation and response serialization.</p></li>
<li><p>Mount Flask, Django, or any WSGI/ASGI app at a subroute.</p></li>
<li><p>Gzip compression, HSTS, CORS, and trusted host validation.</p></li>
<li><p>Before-request hooks that can short-circuit for auth guards.</p></li>
<li><p>Before-request and after-request hooks for auth and logging.</p></li>
<li><p>A test client for fast, in-process testing with pytest.</p></li>
<li><p>Route parameters with f-string syntax and type convertors.</p></li>
<li><p>Lifespan context managers for startup and shutdown logic.</p></li>
<li><p>Custom exception handlers for clean error responses.</p></li>
<li><p><a class="reference external" href="https://graphql.org/">GraphQL</a> with Graphene and a built-in GraphiQL IDE.</p></li>
<li><p>Server-Sent Events for real-time streaming.</p></li>
<li><p>File serving with automatic content-type detection.</p></li>
<li><p>Sync and async views — <code class="docutils literal notranslate"><span class="pre">async</span></code> is always optional.</p></li>
<li><p>Class-based views with <code class="docutils literal notranslate"><span class="pre">on_get</span></code>, <code class="docutils literal notranslate"><span class="pre">on_post</span></code>, <code class="docutils literal notranslate"><span class="pre">on_request</span></code>.</p></li>
<li><p>Built-in rate limiting with <code class="docutils literal notranslate"><span class="pre">X-RateLimit</span></code> headers.</p></li>
<li><p>Content negotiation: JSON, YAML, and MessagePack.</p></li>
<li><p>A pleasant API with a single import statement.</p></li>
<li><p>OpenAPI schema generation with Swagger UI.</p></li>
<li><p>A production <a class="reference external" href="https://www.uvicorn.org/">uvicorn</a> server, ready to deploy.</p></li>
<li><p>HTTP method filtering for REST APIs.</p></li>
<li><p>Route groups for API versioning.</p></li>
<li><p>Signed cookie-based sessions.</p></li>
<li><p>Background tasks in a thread pool.</p></li>
<li><p>WebSocket support.</p></li>
@@ -152,6 +156,8 @@ work with — welcome.</p>
<li class="toctree-l2"><a class="reference internal" href="tour.html#trusted-hosts">Trusted Hosts</a></li>
<li class="toctree-l2"><a class="reference internal" href="tour.html#request-id">Request ID</a></li>
<li class="toctree-l2"><a class="reference internal" href="tour.html#rate-limiting">Rate Limiting</a></li>
<li class="toctree-l2"><a class="reference internal" href="tour.html#pydantic-validation">Pydantic Validation</a></li>
<li class="toctree-l2"><a class="reference internal" href="tour.html#content-negotiation">Content Negotiation</a></li>
<li class="toctree-l2"><a class="reference internal" href="tour.html#messagepack">MessagePack</a></li>
</ul>
</li>
@@ -159,8 +165,11 @@ work with — welcome.</p>
<li class="toctree-l2"><a class="reference internal" href="deployment.html#running-locally">Running Locally</a></li>
<li class="toctree-l2"><a class="reference internal" href="deployment.html#docker">Docker</a></li>
<li class="toctree-l2"><a class="reference internal" href="deployment.html#cloud-platforms">Cloud Platforms</a></li>
<li class="toctree-l2"><a class="reference internal" href="deployment.html#health-check-endpoint">Health Check Endpoint</a></li>
<li class="toctree-l2"><a class="reference internal" href="deployment.html#uvicorn-directly">Uvicorn Directly</a></li>
<li class="toctree-l2"><a class="reference internal" href="deployment.html#docker-compose">Docker Compose</a></li>
<li class="toctree-l2"><a class="reference internal" href="deployment.html#reverse-proxy">Reverse Proxy</a></li>
<li class="toctree-l2"><a class="reference internal" href="deployment.html#production-checklist">Production Checklist</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="testing.html">Testing</a><ul>
@@ -174,6 +183,8 @@ work with — welcome.</p>
<li class="toctree-l2"><a class="reference internal" href="testing.html#testing-error-handling">Testing Error Handling</a></li>
<li class="toctree-l2"><a class="reference internal" href="testing.html#testing-lifespan-events">Testing Lifespan Events</a></li>
<li class="toctree-l2"><a class="reference internal" href="testing.html#testing-before-and-after-hooks">Testing Before and After Hooks</a></li>
<li class="toctree-l2"><a class="reference internal" href="testing.html#testing-rate-limiting">Testing Rate Limiting</a></li>
<li class="toctree-l2"><a class="reference internal" href="testing.html#testing-mounted-apps">Testing Mounted Apps</a></li>
<li class="toctree-l2"><a class="reference internal" href="testing.html#tips">Tips</a></li>
</ul>
</li>
@@ -193,6 +204,7 @@ work with — welcome.</p>
<li class="toctree-l2"><a class="reference internal" href="cli.html#launching-from-a-file">Launching from a File</a></li>
<li class="toctree-l2"><a class="reference internal" href="cli.html#launching-from-a-url">Launching from a URL</a></li>
<li class="toctree-l2"><a class="reference internal" href="cli.html#custom-instance-names">Custom Instance Names</a></li>
<li class="toctree-l2"><a class="reference internal" href="cli.html#environment-variables">Environment Variables</a></li>
<li class="toctree-l2"><a class="reference internal" href="cli.html#building-frontend-assets">Building Frontend Assets</a></li>
</ul>
</li>
@@ -251,6 +263,7 @@ work with — welcome.</p>
<li class="toctree-l2"><a class="reference internal" href="tutorial-middleware.html#built-in-middleware">Built-in Middleware</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial-middleware.html#adding-third-party-middleware">Adding Third-Party Middleware</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial-middleware.html#middleware-order">Middleware Order</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial-middleware.html#writing-pure-asgi-middleware">Writing Pure ASGI Middleware</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial-middleware.html#when-to-use-what">When to Use What</a></li>
</ul>
</li>
+1 -1
View File
File diff suppressed because one or more lines are too long
+53
View File
@@ -295,6 +295,55 @@ just like in production. You can verify their effects on the response:</p>
</pre></div>
</div>
</section>
<section id="testing-rate-limiting">
<h2>Testing Rate Limiting<a class="headerlink" href="#testing-rate-limiting" title="Link to this heading"></a></h2>
<p>Rate limiters are just hooks — they run automatically during tests.
Verify the headers and the 429 response:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">responder.ext.ratelimit</span><span class="w"> </span><span class="kn">import</span> <span class="n">RateLimiter</span>
<span class="k">def</span><span class="w"> </span><span class="nf">test_rate_limiting</span><span class="p">():</span>
<span class="n">api</span> <span class="o">=</span> <span class="n">responder</span><span class="o">.</span><span class="n">API</span><span class="p">(</span><span class="n">allowed_hosts</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;localhost&quot;</span><span class="p">])</span>
<span class="n">limiter</span> <span class="o">=</span> <span class="n">RateLimiter</span><span class="p">(</span><span class="n">requests</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">period</span><span class="o">=</span><span class="mi">60</span><span class="p">)</span>
<span class="n">limiter</span><span class="o">.</span><span class="n">install</span><span class="p">(</span><span class="n">api</span><span class="p">)</span>
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">&quot;/&quot;</span><span class="p">)</span>
<span class="k">def</span><span class="w"> </span><span class="nf">view</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
<span class="n">resp</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="s2">&quot;ok&quot;</span>
<span class="c1"># First two requests succeed</span>
<span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">2</span><span class="p">):</span>
<span class="n">r</span> <span class="o">=</span> <span class="n">api</span><span class="o">.</span><span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;http://localhost/&quot;</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
<span class="k">assert</span> <span class="s2">&quot;X-RateLimit-Remaining&quot;</span> <span class="ow">in</span> <span class="n">r</span><span class="o">.</span><span class="n">headers</span>
<span class="c1"># Third request is rate limited</span>
<span class="n">r</span> <span class="o">=</span> <span class="n">api</span><span class="o">.</span><span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;http://localhost/&quot;</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">429</span>
</pre></div>
</div>
</section>
<section id="testing-mounted-apps">
<h2>Testing Mounted Apps<a class="headerlink" href="#testing-mounted-apps" title="Link to this heading"></a></h2>
<p>When testing WSGI apps mounted at a subroute, use <code class="docutils literal notranslate"><span class="pre">localhost</span></code> as the
host to avoid Werkzeugs trusted host validation:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">flask</span><span class="w"> </span><span class="kn">import</span> <span class="n">Flask</span>
<span class="k">def</span><span class="w"> </span><span class="nf">test_flask_mount</span><span class="p">():</span>
<span class="n">api</span> <span class="o">=</span> <span class="n">responder</span><span class="o">.</span><span class="n">API</span><span class="p">(</span><span class="n">allowed_hosts</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;localhost&quot;</span><span class="p">])</span>
<span class="n">flask_app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
<span class="nd">@flask_app</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">&quot;/&quot;</span><span class="p">)</span>
<span class="k">def</span><span class="w"> </span><span class="nf">hello</span><span class="p">():</span>
<span class="k">return</span> <span class="s2">&quot;Hello from Flask!&quot;</span>
<span class="n">api</span><span class="o">.</span><span class="n">mount</span><span class="p">(</span><span class="s2">&quot;/flask&quot;</span><span class="p">,</span> <span class="n">flask_app</span><span class="p">)</span>
<span class="n">r</span> <span class="o">=</span> <span class="n">api</span><span class="o">.</span><span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;http://localhost/flask&quot;</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
<span class="k">assert</span> <span class="s2">&quot;Hello from Flask&quot;</span> <span class="ow">in</span> <span class="n">r</span><span class="o">.</span><span class="n">text</span>
</pre></div>
</div>
</section>
<section id="tips">
<h2>Tips<a class="headerlink" href="#tips" title="Link to this heading"></a></h2>
<ul class="simple">
@@ -307,6 +356,8 @@ in the test rather than sharing the fixture.</p></li>
thing, but it makes refactoring painless.</p></li>
<li><p><strong>Test the contract, not the implementation.</strong> Assert on status codes,
response bodies, and headers — not on internal state.</p></li>
<li><p><strong>Use ``localhost`` for mounted WSGI apps.</strong> Werkzeug 3.1.7+ validates
the <code class="docutils literal notranslate"><span class="pre">Host</span></code> header, so avoid synthetic hosts like <code class="docutils literal notranslate"><span class="pre">;</span></code> in tests.</p></li>
</ul>
</section>
</section>
@@ -347,6 +398,8 @@ response bodies, and headers — not on internal state.</p></li>
<li><a class="reference internal" href="#testing-error-handling">Testing Error Handling</a></li>
<li><a class="reference internal" href="#testing-lifespan-events">Testing Lifespan Events</a></li>
<li><a class="reference internal" href="#testing-before-and-after-hooks">Testing Before and After Hooks</a></li>
<li><a class="reference internal" href="#testing-rate-limiting">Testing Rate Limiting</a></li>
<li><a class="reference internal" href="#testing-mounted-apps">Testing Mounted Apps</a></li>
<li><a class="reference internal" href="#tips">Tips</a></li>
</ul>
</li>
+72 -2
View File
@@ -581,6 +581,70 @@ response with a <code class="docutils literal notranslate"><span class="pre">Ret
can pace themselves.</p>
<p>The rate limiter is per-client, keyed by IP address.</p>
</section>
<section id="pydantic-validation">
<h2>Pydantic Validation<a class="headerlink" href="#pydantic-validation" title="Link to this heading"></a></h2>
<p><a class="reference external" href="https://docs.pydantic.dev/">Pydantic</a> models integrate directly with
Responders routing. Set <code class="docutils literal notranslate"><span class="pre">request_model</span></code> to validate incoming data and
<code class="docutils literal notranslate"><span class="pre">response_model</span></code> to control the shape of outgoing data:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">pydantic</span><span class="w"> </span><span class="kn">import</span> <span class="n">BaseModel</span>
<span class="k">class</span><span class="w"> </span><span class="nc">ItemIn</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">price</span><span class="p">:</span> <span class="nb">float</span>
<span class="k">class</span><span class="w"> </span><span class="nc">ItemOut</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">price</span><span class="p">:</span> <span class="nb">float</span>
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">&quot;/items&quot;</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;POST&quot;</span><span class="p">],</span>
<span class="n">request_model</span><span class="o">=</span><span class="n">ItemIn</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">ItemOut</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">create_item</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
<span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">req</span><span class="o">.</span><span class="n">media</span><span class="p">()</span>
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&quot;id&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="o">**</span><span class="n">data</span><span class="p">}</span>
</pre></div>
</div>
<p>When <code class="docutils literal notranslate"><span class="pre">request_model</span></code> is set:</p>
<ul class="simple">
<li><p>Valid requests are parsed and the data is available via <code class="docutils literal notranslate"><span class="pre">await</span> <span class="pre">req.media()</span></code></p></li>
<li><p>Invalid requests get an automatic <code class="docutils literal notranslate"><span class="pre">422</span> <span class="pre">Unprocessable</span> <span class="pre">Entity</span></code> response
with detailed error messages — you dont write any validation code</p></li>
</ul>
<p>When <code class="docutils literal notranslate"><span class="pre">response_model</span></code> is set:</p>
<ul class="simple">
<li><p>The response is serialized through the model before being sent</p></li>
<li><p>Extra fields are stripped automatically</p></li>
<li><p>Type coercion happens at the boundary</p></li>
</ul>
<p>This is the recommended way to build validated REST APIs with Responder.
See the <a class="reference internal" href="tutorial-rest.html"><span class="doc">Building a REST API</span></a> for a complete walkthrough.</p>
</section>
<section id="content-negotiation">
<h2>Content Negotiation<a class="headerlink" href="#content-negotiation" title="Link to this heading"></a></h2>
<p>Responder automatically negotiates the response format based on the
clients <code class="docutils literal notranslate"><span class="pre">Accept</span></code> header. Set <code class="docutils literal notranslate"><span class="pre">resp.media</span></code> to a Python object and
the right thing happens:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">Accept:</span> <span class="pre">application/json</span></code> (default) → JSON</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">Accept:</span> <span class="pre">application/x-yaml</span></code> → YAML</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">Accept:</span> <span class="pre">application/x-msgpack</span></code> → MessagePack</p></li>
</ul>
<p>This means a single endpoint serves multiple formats without any
conditional logic in your code:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">&quot;/data&quot;</span><span class="p">)</span>
<span class="k">def</span><span class="w"> </span><span class="nf">data</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&quot;key&quot;</span><span class="p">:</span> <span class="s2">&quot;value&quot;</span><span class="p">}</span>
</pre></div>
</div>
<p>Clients get the format they ask for:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ curl http://localhost:5042/data
{&quot;key&quot;: &quot;value&quot;}
$ curl -H &quot;Accept: application/x-yaml&quot; http://localhost:5042/data
key: value
</pre></div>
</div>
</section>
<section id="messagepack">
<h2>MessagePack<a class="headerlink" href="#messagepack" title="Link to this heading"></a></h2>
<p><a class="reference external" href="https://msgpack.org/">MessagePack</a> is a binary serialization format
@@ -589,11 +653,15 @@ high-throughput APIs, IoT devices, and anywhere bandwidth matters.</p>
<p>Responder supports MessagePack alongside JSON and YAML:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Decode a MessagePack request body</span>
<span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">req</span><span class="o">.</span><span class="n">media</span><span class="p">(</span><span class="s2">&quot;msgpack&quot;</span><span class="p">)</span>
<span class="c1"># Respond with MessagePack</span>
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&quot;result&quot;</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]}</span>
</pre></div>
</div>
<p>Content negotiation works too — clients can send
<p>Content negotiation works automatically — clients can send
<code class="docutils literal notranslate"><span class="pre">Accept:</span> <span class="pre">application/x-msgpack</span></code> to receive MessagePack responses
instead of JSON.</p>
instead of JSON. You can also explicitly decode MessagePack request
bodies by passing <code class="docutils literal notranslate"><span class="pre">&quot;msgpack&quot;</span></code> to <code class="docutils literal notranslate"><span class="pre">req.media()</span></code>.</p>
</section>
</section>
@@ -644,6 +712,8 @@ instead of JSON.</p>
<li><a class="reference internal" href="#trusted-hosts">Trusted Hosts</a></li>
<li><a class="reference internal" href="#request-id">Request ID</a></li>
<li><a class="reference internal" href="#rate-limiting">Rate Limiting</a></li>
<li><a class="reference internal" href="#pydantic-validation">Pydantic Validation</a></li>
<li><a class="reference internal" href="#content-negotiation">Content Negotiation</a></li>
<li><a class="reference internal" href="#messagepack">MessagePack</a></li>
</ul>
</li>
+39
View File
@@ -145,6 +145,44 @@ and the response last.</p>
the existing stack. Keep this in mind for ordering dependencies — if
middleware A depends on middleware B having run first, add B before A.</p>
</section>
<section id="writing-pure-asgi-middleware">
<h2>Writing Pure ASGI Middleware<a class="headerlink" href="#writing-pure-asgi-middleware" title="Link to this heading"></a></h2>
<p>For maximum performance and control, you can write middleware as a plain
ASGI application. This bypasses Starlettes <code class="docutils literal notranslate"><span class="pre">BaseHTTPMiddleware</span></code>
abstraction — its faster and gives you direct access to the ASGI
protocol:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">class</span><span class="w"> </span><span class="nc">SecurityHeadersMiddleware</span><span class="p">:</span>
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">app</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">app</span> <span class="o">=</span> <span class="n">app</span>
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">send</span><span class="p">):</span>
<span class="k">if</span> <span class="n">scope</span><span class="p">[</span><span class="s2">&quot;type&quot;</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">&quot;http&quot;</span><span class="p">:</span>
<span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="p">(</span><span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">send</span><span class="p">)</span>
<span class="k">return</span>
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">send_with_headers</span><span class="p">(</span><span class="n">message</span><span class="p">):</span>
<span class="k">if</span> <span class="n">message</span><span class="p">[</span><span class="s2">&quot;type&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;http.response.start&quot;</span><span class="p">:</span>
<span class="n">extra</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="sa">b</span><span class="s2">&quot;x-content-type-options&quot;</span><span class="p">,</span> <span class="sa">b</span><span class="s2">&quot;nosniff&quot;</span><span class="p">),</span>
<span class="p">(</span><span class="sa">b</span><span class="s2">&quot;x-frame-options&quot;</span><span class="p">,</span> <span class="sa">b</span><span class="s2">&quot;DENY&quot;</span><span class="p">),</span>
<span class="p">(</span><span class="sa">b</span><span class="s2">&quot;referrer-policy&quot;</span><span class="p">,</span> <span class="sa">b</span><span class="s2">&quot;strict-origin-when-cross-origin&quot;</span><span class="p">),</span>
<span class="p">]</span>
<span class="n">message</span><span class="p">[</span><span class="s2">&quot;headers&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">message</span><span class="p">[</span><span class="s2">&quot;headers&quot;</span><span class="p">])</span> <span class="o">+</span> <span class="n">extra</span>
<span class="k">await</span> <span class="n">send</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">app</span><span class="p">(</span><span class="n">scope</span><span class="p">,</span> <span class="n">receive</span><span class="p">,</span> <span class="n">send_with_headers</span><span class="p">)</span>
<span class="n">api</span><span class="o">.</span><span class="n">add_middleware</span><span class="p">(</span><span class="n">SecurityHeadersMiddleware</span><span class="p">)</span>
</pre></div>
</div>
<p>This is the same pattern used internally by Starlette and uvicorn. The
middleware receives the ASGI <code class="docutils literal notranslate"><span class="pre">scope</span></code>, <code class="docutils literal notranslate"><span class="pre">receive</span></code>, and <code class="docutils literal notranslate"><span class="pre">send</span></code> callables,
and wraps <code class="docutils literal notranslate"><span class="pre">send</span></code> to inject headers into the response.</p>
<p>For most cases, <code class="docutils literal notranslate"><span class="pre">BaseHTTPMiddleware</span></code> 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.</p>
</section>
<section id="when-to-use-what">
<h2>When to Use What<a class="headerlink" href="#when-to-use-what" title="Link to this heading"></a></h2>
<ul class="simple">
@@ -189,6 +227,7 @@ middleware when hooks arent enough.</p>
<li><a class="reference internal" href="#built-in-middleware">Built-in Middleware</a></li>
<li><a class="reference internal" href="#adding-third-party-middleware">Adding Third-Party Middleware</a></li>
<li><a class="reference internal" href="#middleware-order">Middleware Order</a></li>
<li><a class="reference internal" href="#writing-pure-asgi-middleware">Writing Pure ASGI Middleware</a></li>
<li><a class="reference internal" href="#when-to-use-what">When to Use What</a></li>
</ul>
</li>