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 `_, 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)