Files
responder/docs/source/runtime-contracts.rst

236 lines
9.6 KiB
ReStructuredText

Runtime Contracts
=================
Responder keeps a small set of runtime contracts stable across the 9.x series.
This page collects the behaviors that application code, tests, generated
clients, and OpenAPI consumers can rely on.
Error Responses
---------------
Framework-generated errors use ``application/problem+json`` by default. The
payload always includes:
- ``type``: ``"about:blank"`` unless an application ``problem_handler`` changes
it or the error was raised as a ``responder.Problem`` with an explicit
``type``.
- ``title``: the HTTP status title.
- ``status``: the numeric HTTP status code.
When extra context exists, the payload may also include:
- ``detail`` for human-readable context.
- ``errors`` for structured validation failures.
- ``request_id`` when request ID middleware or structured logging has attached
one to the request scope.
- ``instance`` (defaulting to the request path) plus any extension members when
the error was raised as a ``responder.Problem``.
The standard framework error statuses are:
.. list-table::
:header-rows: 1
* - Status
- Default title
- Notes
* - ``400``
- ``Bad Request``
- Malformed request bodies, such as invalid JSON.
* - ``401``
- ``Unauthorized``
- Authentication failed or credentials were missing.
* - ``403``
- ``Forbidden``
- Authentication succeeded, but the principal lacks required scope.
* - ``404``
- ``Not Found``
- No route matched the request path.
* - ``405``
- ``Method Not Allowed``
- The path exists but does not accept the request method.
* - ``413``
- ``Content Too Large``
- ``max_request_size`` rejected the body.
* - ``422``
- ``Validation Error``
- Path, query, header, cookie, form, file, or body validation failed.
* - ``500``
- ``Internal Server Error``
- Unhandled application errors and response-model validation failures.
* - ``504``
- ``Gateway Timeout``
- ``request_timeout`` expired before the handler finished.
Pass ``API(problem_details=False)`` to keep the legacy behavior. In that mode,
requests that ask for JSON receive ``{"error": "..."}``, while validation
failures receive ``{"errors": [...]}``. Requests that do not ask for JSON may
receive plain text, matching older Responder releases.
``API(problem_handler=...)`` can enrich or replace problem-details payloads. The
handler receives ``(payload, request, exc)``; returning ``None`` means the
payload was mutated in place. If the handler raises or returns a non-dict value,
Responder logs the failure and uses the original framework payload.
Authentication
--------------
Application-level auth configured with ``API(auth=...)`` applies to every route
by default. A route can opt out with ``auth=None`` or replace it with
``auth=...`` on the route decorator.
Auth helpers inject the authenticated principal as ``user``, ``principal``, and
``auth`` when the route declares those keyword-only parameters. They also set
``req.state.user`` and ``req.state.auth``.
``auth.optional()`` only treats missing credentials as anonymous. Malformed
credentials, wrong schemes, bad API keys, and failed verification still fail
through the wrapped auth scheme. For scoped auth, anonymous requests are allowed
when credentials are missing, but supplied credentials must still satisfy the
required scopes.
Scoped auth returns ``403`` when a principal is authenticated but does not hold
every required scope or role. If the wrapped scheme provides a challenge,
Responder adds an ``insufficient_scope`` ``WWW-Authenticate`` challenge with the
missing scopes.
``api.policy(name, auth)`` creates a named wrapper around an auth helper. The
policy name is an application label only: authentication, principal injection,
OpenAPI security schemes, optional auth, and scoped requirements all continue to
come from the wrapped helper.
Dependency Injection
--------------------
Responder resolves route dependencies after before-request hooks and auth, and
before the route handler. For HTTP requests, the order is:
1. Before-request hooks.
2. Route or application auth.
3. Route dependency guards.
4. Handler parameter dependencies.
5. Handler.
6. After-request hooks.
7. Request-scoped dependency teardown.
Each dependency is resolved at most once per request. Shared sub-dependencies
reuse the same value within that request.
Generator dependencies tear down in reverse dependency order. Teardown failures
are logged, and remaining teardowns still run.
App-scoped dependencies are resolved once for the application lifetime and are
protected by a lock during first resolution, so concurrent first users share the
same initialized value. If first initialization raises, the failed value is not
cached and a later request may retry. App-scoped teardowns run during lifespan
shutdown and clear the app-scope cache.
App-scoped dependencies may depend on other app-scoped dependencies, but they
cannot receive the request object and cannot depend on request-scoped
dependencies.
Response Contracts
------------------
A supported handler return annotation is the response contract. Pydantic
models, dataclasses, typed dictionaries, collections, unions, pagination
models, and JSON scalars are inferred through Pydantic ``TypeAdapter``. The
same contract drives runtime validation and serialization, OpenAPI, and
generated-client types. ``response_model=...`` declares the same contract
explicitly; ``response_model=False`` disables both inference and validation.
Validation runs after the handler and after-request hooks. Valid responses are
coerced and filtered through the contract. Invalid responses fail closed:
- In normal mode, the client receives a ``500`` framework error.
- In debug mode, the validation exception is raised for the developer.
Response-contract validation failures are passed to ``problem_handler`` as the
``exc`` argument when problem details are enabled.
String and bytes return annotations default to ``text/plain`` and
``application/octet-stream``. Other inferred contracts default to
``application/json``. Error responses (``4xx``/``5xx``) are not validated
against the success contract. Declare a status-specific contract when the
handler owns that response shape::
@api.get("/items/{item_id}", responses={404: NotFound})
def get_item(req, resp, *, item_id: str) -> ItemOut:
if item := find_item(item_id):
return item
return NotFound(detail="Item not found"), 404
For additional OpenAPI metadata, use ``responses={404: {"model": NotFound,
"description": "Item not found"}}``. Status-specific contracts validate the
final body just like the success contract. Informational, ``204``, ``205``,
and ``304`` responses cannot declare a body contract and never send content,
even if a handler assigned or returned a body; ``HEAD`` validates the
corresponding ``GET`` contract but sends headers only.
Typed Event Streams
-------------------
``@api.sse`` and ``@api.ndjson`` extend response contracts to incremental
responses. Annotate a sync or async iterator and Responder infers its item
type through the same Pydantic ``TypeAdapter`` path used for buffered return
annotations::
from collections.abc import AsyncIterator
@api.ndjson("/items")
async def items(req, resp) -> AsyncIterator[ItemOut]:
async for item in store.iter_items():
yield item
Use ``event_model=`` or ``item_model=`` when the return annotation cannot
express the contract. SSE routes may annotate ``AsyncIterator[SSE[ItemOut]]``;
the envelope's ``data`` is validated as ``ItemOut`` while ``event``, ``id``,
``retry``, and comment metadata remain protocol fields.
OpenAPI describes the wire media type and includes the item schema in the
``x-responder-item-schema`` media extension. Responder's generated Python,
JavaScript, TypeScript, Ruby, and PHP clients recognize that extension and
produce lazy streaming iterators rather than buffering the response.
An immediately available first item is validated before headers. Later
validation or producer failures are logged and terminate the stream without
emitting the broken item. Debug mode re-raises the failure. Idle SSE routes
with ``heartbeat=`` begin the response without waiting indefinitely for their
first event, allowing keepalives to flow; their first eventual item is still
validated before it is written.
OpenAPI Defaults
----------------
When OpenAPI is enabled, Responder generates operation IDs, summaries, tags,
request bodies, response schemas, validation responses, auth security
requirements, and common framework error responses from the route contract.
Route decorators can add or override operation metadata with ``responses=``,
``examples=``, ``response_examples=``, and ``openapi_extra=``; nested response
metadata is deep-merged with the generated contract.
``status_code=`` declares the route's default success status: the generated
operation keys its success response under it instead of ``200`` (informational,
``204``, ``205``, and ``304`` statuses document no response body), and
``resp.status_code`` is pre-seeded with it before the handler runs. An explicit
assignment in the handler — or a before-request hook short-circuiting the route
— still wins.
The generated document is cached: registering a route, schema, or security
scheme invalidates the cache, and the next request to the schema route (or
docs page) rebuilds it.
With problem details enabled, generated operations document
``application/problem+json`` responses and the reusable ``ProblemDetails``
component schema. With ``problem_details=False``, generated operations document
the legacy JSON error shape and do not register ``ProblemDetails``.
Generated OpenAPI documents are validated in the test suite for both OpenAPI
3.0.x and 3.1.x.