- 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>
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.