mirror of
https://github.com/kennethreitz/responder.git
synced 2026-07-22 10:39:30 +00:00
588919b64c
The tour and RateLimiter docstring now lead with install()/limit and explain that a hand-rolled check() call enforces identically but is invisible to the OpenAPI generator, so 429/503 won't appear in the schema. The v9 migration guide gains a section on rate-limit errors switching to RFC 9457 problem+json under the default problem_details=True, with the problem_details=False escape hatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
179 lines
7.0 KiB
ReStructuredText
179 lines
7.0 KiB
ReStructuredText
Migrating to v9
|
|
===============
|
|
|
|
Responder 9.0 makes the v8.1 deprecation path the default behavior. Most
|
|
apps only need small cleanup: use the supported test-client helpers, pass
|
|
explicit route endpoints, and rely on precision-preserving JSON defaults.
|
|
|
|
The compatibility switches documented below are explicit and quiet. They are
|
|
there for apps that need to keep 8.x behavior while migrating.
|
|
|
|
Run your test suite with warnings surfaced to find any unrelated deprecations::
|
|
|
|
python -W error::DeprecationWarning -m pytest
|
|
|
|
Multipart uploads stream to disk
|
|
--------------------------------
|
|
|
|
Multipart bodies now parse incrementally as they arrive: file parts spool to
|
|
temporary files (rolling to disk past ~1 MB) instead of being buffered in
|
|
RAM. Nothing changes in how you read uploads — ``File()`` markers,
|
|
``req.media("files")``, and ``req.media("form")`` all work as before, and
|
|
``UploadFile.save()`` copies disk-to-disk in chunks.
|
|
|
|
Two consequences of the body no longer being buffered:
|
|
|
|
* **The raw body is consumed by the parse.** ``await req.content`` after a
|
|
multipart parse raises a ``RuntimeError`` (previously it returned the full
|
|
buffered body). If a handler needs both the raw bytes and the parsed form,
|
|
await ``req.content`` *first* — parsing then falls back to the buffered
|
|
body, exactly like 8.x. Handlers using ``File()``/``Form()`` markers have
|
|
the form parsed before they run, so the raw multipart body is not available
|
|
to them at all (this matches Starlette and FastAPI).
|
|
|
|
* **Text fields share the streaming parser's limits.** ``req.media("form")``
|
|
on a multipart body now goes through the same parse as ``media("files")``:
|
|
at most 1000 parts and 1 MB per text field (oversized or malformed bodies
|
|
get a ``400``). Text fields with invalid UTF-8 are now decoded lossily
|
|
instead of being silently dropped.
|
|
|
|
Request bodies are capped at 100 MiB by default
|
|
-----------------------------------------------
|
|
|
|
``max_request_size`` now defaults to 100 MiB instead of unlimited; larger
|
|
bodies get a ``413``. The cap is enforced chunk-by-chunk on the wire, and
|
|
since multipart uploads spool to disk, raising it does not raise memory
|
|
use::
|
|
|
|
api = responder.API(max_request_size=5 * 1024**3) # allow 5 GiB uploads
|
|
api = responder.API(max_request_size=None) # pre-9.0 unlimited
|
|
|
|
Rate-limit errors use Problem Details
|
|
-------------------------------------
|
|
|
|
With the default ``API(problem_details=True)``, the rate limiter's
|
|
``429 Too Many Requests`` and fail-closed-backend ``503 Service
|
|
Unavailable`` responses now use the same RFC 9457
|
|
``application/problem+json`` envelope as framework-generated errors::
|
|
|
|
{"type": "about:blank", "title": "Too Many Requests",
|
|
"status": 429, "detail": "Rate limit exceeded."}
|
|
|
|
Previously the body was ``{"error": "rate limit exceeded"}`` — clients that
|
|
parse the ``error`` key need updating (or key off the status code and
|
|
``Retry-After``/``X-RateLimit-*`` headers, which are unchanged). To keep the
|
|
legacy body shape while migrating, pass ``API(problem_details=False)``,
|
|
which also keeps the legacy shape for all framework errors.
|
|
|
|
``trust_proxy_headers`` now rewrites the connection scope
|
|
----------------------------------------------------------
|
|
|
|
In 8.x, ``API(trust_proxy_headers=True)`` only changed the client IP recorded
|
|
by ``enable_logging``. It now honors the full set of forwarding headers —
|
|
RFC 7239 ``Forwarded``, plus ``X-Forwarded-Proto``/``-Host``/``-For`` and
|
|
``X-Real-IP`` — rewriting the request's scheme, host, and client address for
|
|
every layer: HTTPS detection, redirects, URL building, trusted-host
|
|
validation, and rate-limit keys. If you enabled the flag purely for logging,
|
|
logged IPs are unchanged; the scheme/host handling simply becomes correct
|
|
behind your proxy. Leave the flag off (the default) when Responder is
|
|
directly exposed.
|
|
|
|
``api.session()`` was removed
|
|
-----------------------------
|
|
|
|
The legacy test-client accessor ``api.session()`` has been removed. Use the
|
|
:attr:`~responder.API.requests` property instead::
|
|
|
|
r = api.requests.get("http://;/hello")
|
|
|
|
If you relied on ``session(base_url=...)`` for a custom base URL, construct
|
|
the client through the supported helper::
|
|
|
|
client = api.test_client(base_url="http://testserver")
|
|
|
|
Explicit ``port=`` now wins over ``PORT``
|
|
-----------------------------------------
|
|
|
|
When ``api.run()`` / ``api.serve()`` receive both an explicit ``port=`` and
|
|
a conflicting ``PORT`` environment variable, the explicit argument wins::
|
|
|
|
# PORT=9000 in the environment:
|
|
api.run(port=8000) # binds 8000
|
|
api.run(port=9000) # binds 9000
|
|
api.run() # binds 9000
|
|
|
|
To keep environment-first behavior, resolve the environment yourself::
|
|
|
|
api.run(port=int(os.environ.get("PORT", 8000)))
|
|
|
|
or use the legacy compatibility switch while migrating::
|
|
|
|
api.run(port=8000, port_precedence="env")
|
|
|
|
Bare ``add_route()`` static fallback
|
|
------------------------------------
|
|
|
|
Calling ``api.add_route(route)`` with no endpoint now raises by default
|
|
instead of implicitly registering a *default* route that serves
|
|
``static/index.html`` for every unmatched request. Pass an endpoint
|
|
explicitly instead::
|
|
|
|
import pathlib
|
|
|
|
async def spa(req, resp):
|
|
resp.html = (pathlib.Path("static") / "index.html").read_text()
|
|
|
|
api.add_route("/", spa, default=True)
|
|
|
|
Static *assets* are unaffected: the ``static_dir`` / ``static_route`` mount
|
|
keeps working as-is.
|
|
|
|
If you need the old fallback while migrating, opt in explicitly::
|
|
|
|
api = responder.API(implicit_static_fallback=True)
|
|
|
|
``Decimal`` serializes as a JSON string
|
|
---------------------------------------
|
|
|
|
Assigning a bare :class:`decimal.Decimal` to ``resp.media`` now serializes it
|
|
as a JSON string, preserving precision.
|
|
|
|
Choose a representation explicitly when your API contract needs a number::
|
|
|
|
resp.media = {"price": str(total)} # exact, v9's default
|
|
resp.media = {"price": float(total)} # lossy, JSON number
|
|
|
|
or keep floats everywhere with the compatibility flag or a custom encoder::
|
|
|
|
api = responder.API(json_decimal="float")
|
|
|
|
def encoder(obj):
|
|
if isinstance(obj, decimal.Decimal):
|
|
return float(obj)
|
|
raise TypeError # fall back to the built-in conversions
|
|
|
|
api = responder.API(encoder=encoder)
|
|
|
|
GraphQL: ``400`` with partial data
|
|
-----------------------------------
|
|
|
|
The GraphQL extension now returns HTTP ``200`` for execution results that
|
|
contain both ``data`` and ``errors`` (a *partial* result, e.g. one resolver
|
|
failed while others succeeded). Per the `GraphQL-over-HTTP specification
|
|
<https://graphql.github.io/graphql-over-http/>`_, these are well-formed
|
|
GraphQL responses.
|
|
|
|
Requests that produce *no* data (validation or request errors) keep their
|
|
``400``. Inspect the ``errors`` key of the response body instead of relying
|
|
on the status code alone::
|
|
|
|
api.graphql("/graph", schema=schema)
|
|
|
|
result = client.post("/graph", json={"query": query}).json()
|
|
if result.get("errors"):
|
|
... # handle errors, regardless of HTTP status
|
|
|
|
To preserve the legacy partial-data status while migrating, pass::
|
|
|
|
api.graphql("/graph", schema=schema, partial_data_status=400)
|