mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
deploy: 3d5f3c7e93
This commit is contained in:
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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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()``.
|
||||
|
||||
@@ -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
|
||||
-----------------
|
||||
|
||||
|
||||
+5
-4
@@ -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>
|
||||
|
||||
@@ -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">"SECRET_KEY"</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>
|
||||
|
||||
@@ -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">"/health"</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">"status"</span><span class="p">:</span> <span class="s2">"healthy"</span><span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>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.</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">"5042:80"</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>Responder’s <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 uvicorn’s <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
@@ -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
File diff suppressed because one or more lines are too long
@@ -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">"localhost"</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">"/"</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">"ok"</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">"http://localhost/"</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">"X-RateLimit-Remaining"</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">"http://localhost/"</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 Werkzeug’s 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">"localhost"</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">"/"</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">"Hello from Flask!"</span>
|
||||
|
||||
<span class="n">api</span><span class="o">.</span><span class="n">mount</span><span class="p">(</span><span class="s2">"/flask"</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">"http://localhost/flask"</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">"Hello from Flask"</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>
|
||||
|
||||
@@ -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
|
||||
Responder’s 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">"/items"</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s2">"POST"</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">"id"</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 don’t 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
|
||||
client’s <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">"/data"</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">"key"</span><span class="p">:</span> <span class="s2">"value"</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
|
||||
{"key": "value"}
|
||||
|
||||
$ curl -H "Accept: application/x-yaml" 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">"msgpack"</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">"result"</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">"msgpack"</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>
|
||||
|
||||
@@ -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 Starlette’s <code class="docutils literal notranslate"><span class="pre">BaseHTTPMiddleware</span></code>
|
||||
abstraction — it’s 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">"type"</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">"http"</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">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"http.response.start"</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">"x-content-type-options"</span><span class="p">,</span> <span class="sa">b</span><span class="s2">"nosniff"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="sa">b</span><span class="s2">"x-frame-options"</span><span class="p">,</span> <span class="sa">b</span><span class="s2">"DENY"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="sa">b</span><span class="s2">"referrer-policy"</span><span class="p">,</span> <span class="sa">b</span><span class="s2">"strict-origin-when-cross-origin"</span><span class="p">),</span>
|
||||
<span class="p">]</span>
|
||||
<span class="n">message</span><span class="p">[</span><span class="s2">"headers"</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">"headers"</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 aren’t 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>
|
||||
|
||||
Reference in New Issue
Block a user