Files
kennethreitz d934334d0e v5: fix 7 bugs found by the adversarial review of the v5 diff
- resp.session = {...} no longer 500s on the default cookie backend: the
  setter mutates Starlette's Session in place (clear+update) instead of
  rebinding it to a plain dict.
- Class-based view markers now resolve (per-view marker resolution in
  run_views) instead of leaking the _Marker sentinel as the argument.
- OpenAPI: _body_model mirrors the runtime exclusions (skips dependency,
  path, and defaulted params), so a dependency-injected Pydantic param is no
  longer documented as a phantom requestBody + 422.
- OpenAPI: a request_model route with no methods= is documented as POST (not
  GET-only), and 422 is scoped to methods that actually validate.
- Path() marker alias is honored (raw_value reads by lookup) and the raw URL
  key no longer leaks as an unexpected kwarg.
- Bare list/set/tuple/frozenset query annotations are treated as multi-value.

Adds the v5 migration guide and regression tests for every fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:19:15 -04:00

4.5 KiB

Migrating to Responder 5.0

Responder 5 layers fully type-driven request/response I/O, composable dependency injection, plan-driven OpenAPI, secure-by-default sessions, and a deferred middleware stack onto the unchanged (req, resp) core. The new typed features are additive sugar — your existing handlers keep working — but v5 makes a handful of deliberate breaking changes, listed here with the one-line fix.

Sessions

Change What to do
secret_key no longer defaults to the public "NOTASECRET"; API() mints a random per-process key Set API(secret_key=…) or the RESPONDER_SECRET_KEY env var for stable, multi-worker sessions (a startup warning fires until you do).
API(secret_key="NOTASECRET") raises SessionConfigError Generate a real key: python -c "import secrets; print(secrets.token_urlsafe(32))".
Session cookies are Secure by default in production No action behind a TLS proxy; pass session_https_only=False only if you genuinely serve plain HTTP (e.g. local dev, tests).
req.session / resp.session raise RuntimeError when sessions=False Re-enable sessions, or stop reading the session under the explicit opt-out.
sessions=True with no key raises Provide a key, or use sessions="auto" (the default) to auto-generate an ephemeral one.

Server-side sessions now slide their TTL via touch/atouch on read-only requests (no behavior change for you; implement touch on a custom backend for the cheaper path).

req.method is uppercase

req.method now returns "GET", not "get" (matching Flask/FastAPI/Starlette). For one deprecation cycle it compares case-insensitively, so req.method == "get" keeps working with a DeprecationWarning. Uppercase your literals.

Hash-based membership is the one thing the shim can't save: req.method in {"get"} and {"get": …}[req.method] miss silently — use ==, a tuple/list, or uppercase keys.

Middleware & errors

Change What to do
API.app is a lazily-built read-only property, not a writable attribute Mutate via api.add_middleware(Cls, **opts) (now valid post-construction), or wrap the API object: asgi = MyMiddleware(api).
User middleware now sits inside ServerErrorMiddleware Its exceptions are now caught and rendered as 500s. To wrap everything, wrap the API object instead of add_middleware.
Session writes are not persisted on an unhandled 500 Persist explicitly before raising if you need it.

X-Request-ID now appears on error/500 responses too, and api.add_exception_handler(exc_or_status, handler) is now a first-class method.

Typed handler I/O

Change What to do
A Pydantic return annotation (-> Model) is now honored as response_model Make the returned dict conform, or pass @api.route(..., response_model=False) (if you don't want validation).
A body-model parameter beats a same-named dependency Rename the dependency, or give the parameter a default.
Query/Header/Cookie/Path are reserved top-level names Only affects from responder import *; explicit imports are unaffected.

New: def search(req, resp, *, q: str = Query(...), token: str = Header(None)) injects validated query params, headers, cookies, and path params.

Dependency injection

Change What to do
A provider receives the request only via a req/request parameter or a Request/WebSocket annotation Rename a nonstandard request parameter to req, or annotate it Request. A sole-unnamed-param provider still works for one cycle (with a DeprecationWarning).
req/request/resp/response/ws/websocket are reserved dependency names Rename any dependency registered under these.
Misconfigured graphs raise DependencyError subclasses Cycles, unknown params, and scope violations now surface as DependencyCycleError / DependencyResolutionError / DependencyScopeError.

New: a provider can depend on other providers (recursive, memoized, reverse-topological teardown, cycle detection).

OpenAPI

Change What to do
Routes without a docstring/model now appear in the schema Pass @api.route(..., include_in_schema=False) to hide a route (schema/docs/static/metrics are auto-excluded).
Docstring YAML is deep-merged onto a generated base Fully-specified docstrings are unaffected; the generated parameters/request/response are added underneath.

The spec is now generated from each route's methods, models, and Query/Header/Cookie markers, with an automatic 422 for validating routes.