Migrating to v7¶
Responder 7.0 focuses on explicit runtime contracts: framework errors use
problem details by default, auth can be declared at the app level, dependencies
can be attached to routes as graph-aware guards, and optional production server
tooling lives behind the server extra.
Problem Details by Default¶
Framework-generated errors now use RFC 9457-style
application/problem+json responses by default:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "Not Found"
}
This applies to framework errors such as 404, 405, request parsing failures,
validation failures, request timeouts, auth failures, and production
response-model validation failures. When validation details exist, the framework
still includes them as the extension member errors:
{
"type": "about:blank",
"title": "Validation Error",
"status": 422,
"detail": "Validation failed",
"errors": [{...}],
}
If you need the previous content-negotiated behavior while migrating, pass
problem_details=False:
api = responder.API(problem_details=False)
App-Level Auth¶
Routes can still declare auth directly:
@api.get("/me", auth=bearer)
def me(req, resp, *, user):
resp.media = {"user": user}
In v7, apps can also define a default auth scheme. Routes inherit it unless
they explicitly opt out with auth=None:
api = responder.API(auth=bearer)
@api.post("/login", auth=None)
def login(req, resp):
resp.media = {"token": issue_token()}
@api.get("/me")
def me(req, resp, *, user):
resp.media = {"user": user}
When OpenAPI is enabled, inherited auth is documented on protected operations
and auth=None marks public operations with an empty security requirement.
Auth helpers can also be wrapped with lightweight scope or role checks:
admin = bearer.requires("items:write")
@api.post("/items", auth=admin)
def create_item(req, resp, *, user):
resp.media = {"user": user}
Responder reads scopes from a principal’s scopes or roles attribute/key.
Missing scopes produce 403 and scoped OpenAPI security requirements are
documented on the operation.
Route Dependency Guards¶
Depends(...) still injects local dependency values into handler parameters.
Use route-level dependencies=[Depends(...)] when the dependency is a guard
or setup step and the handler does not need its return value. This keeps the
call in the dependency graph with caching/teardown behavior, and still works
for side effects:
def require_user(req):
if "Authorization" not in req.headers:
responder.abort(401, detail="Not authenticated")
@api.get("/private", dependencies=[Depends(require_user)])
def private(req, resp):
resp.media = {"ok": True}
Route dependencies follow the same lifecycle rules as parameter dependencies, including sync/async providers, sub-dependencies, and generator teardown.
For raw before/after hooks, use before=/after= to keep intent explicit.
Hooks are not dependency providers: they run around the handler for
request/response mutation or short-circuiting, while Depends(...) remains
the path for dependency caching, sub-dependencies, and generator teardown.
The three route-local mechanisms are distinct:
Handler parameters with
Depends(...)resolve a value and inject it into the handler.dependencies=[Depends(...)]resolves graph-aware providers for side effects and ignores their return value.before=/after=runs raw hooks with no dependency graph participation.
Route execution order is now explicit: global before_request hooks, route
before hooks, auth, validation, route dependencies=..., handler,
response-model checks, and finally after hooks.
This ordering matters if a route has both hooks and dependency-guards.
Request Model Removal¶
request_model= and req.state.validated have been removed. Use a required
Pydantic-typed handler parameter for request-body validation instead:
class ItemIn(BaseModel):
name: str
@api.post("/items")
async def create_item(req, resp, *, item: ItemIn):
resp.media = item.model_dump()
This is the same validation path used by OpenAPI generation and returns 422
before the handler runs when the body is missing fields, has wrong types, or is
not a JSON object.
Server Extra¶
The default install still includes the uvicorn runner used by api.run().
Install responder[server] when you want the optional Granian server too:
uv pip install 'responder[server]'
Then run the current app with Granian’s embedded ASGI server:
api.run(server="granian")
If Granian is not installed, server="granian" raises a runtime error with
the install command. For multi-worker production deployments, point Granian’s
CLI at your ASGI app:
granian --interface asgi --host 0.0.0.0 --port 8000 api:api
Request Method Type¶
req.method now returns an exact str. It is still uppercase
("GET", "POST", etc.) and comparisons remain case-sensitive. The old
exported HTTPMethod subclass has been removed.
Python Support¶
Responder 7.0 requires Python 3.11 or newer; Python 3.10 is no longer supported. CPython 3.11–3.15 (including free-threaded builds) and PyPy 3.11 are tested in CI.