Extension hooks and async boundaries¶
aiodrf preserves DRF's extension points; it does not make every method in DRF
awaitable. Use the pairs below for asynchronous work. Keep synchronous factories
and callbacks synchronous unless their caller explicitly supports an async pair.
Changing an arbitrary def to async def is not a safe migration.
Supported view pairs¶
| Synchronous DRF hook | Async counterpart |
|---|---|
initial |
ainitial |
perform_authentication |
aperform_authentication |
check_permissions |
acheck_permissions |
check_object_permissions |
acheck_object_permissions |
check_throttles |
acheck_throttles |
handle_exception |
ahandle_exception |
finalize_response |
afinalize_response |
get_queryset |
aget_queryset |
get_object |
aget_object |
get_serializer |
aget_serializer |
get_serializer_class |
aget_serializer_class |
get_serializer_context |
aget_serializer_context |
get_authenticate_header |
aget_authenticate_header |
filter_queryset |
afilter_queryset |
paginate_queryset |
apaginate_queryset |
get_paginated_response |
aget_paginated_response |
perform_create |
aperform_create |
perform_update |
aperform_update |
perform_destroy |
aperform_destroy |
get_etag |
aget_etag |
get_last_modified |
aget_last_modified |
get_validated_query_params |
aget_validated_query_params |
The generic-view and persistence hooks require the corresponding generic view
or mixin. HTTP handlers and CRUD actions (list, create, retrieve, update,
partial_update, destroy) keep their ordinary names. They may be coroutines;
there is no view-level alist or acreate convention. Async naming
records why, and aiodrf.W006 reports adrf's action names on a view.
For a supported pair, the nearest override in the MRO wins. If a class defines
both members, the async member wins. A coroutine under the original name is
also awaited by the async dispatcher. Prefer the a... counterpart for a new
typed override: it keeps the original DRF signature usable by synchronous
consumers. Unknown synchronous overrides run in a worker, once; they are not
executed speculatively on the event loop and retried.
Synchronous bridges support DRF's synchronous callers, such as metadata and
schema generation, from synchronous code or a worker. Do not call a synchronous
bridge to an async override directly on an already-running event loop; use
the async counterpart there. Inside an async override, await super().a...().
Serializer factories and authentication challenges¶
Generic views await aget_serializer_class() and aget_serializer_context()
before constructing a serializer in the worker. An explicit context= argument
is retained without calling the context hook. Dynamic serializer classes pass
through the same schema adaptation and backend allowlist checks as static ones.
class ReportView(generics.ListAPIView):
serializer_class = ReportSerializer
queryset = Report.objects.all()
async def aget_serializer_context(self):
context = dict(await super().aget_serializer_context())
context["currency"] = await self.request.pricing.currency()
return context
The example assumes application-defined Report, ReportSerializer and an
async pricing service. Selection hooks may also be used while deriving ORM
prefetch lookups or generating metadata, not only to serialize a response.
Keep them safe to call more than once and provide schema-time defaults.
Serializer factory pairs require GenericAPIView or a generic viewset; adding
an async factory to a nongeneric APIView does not install that protocol.
Authentication classes may implement aauthenticate_header(request); the view
may instead override aget_authenticate_header(request). The first configured
authenticator supplies the default challenge, as in DRF. A nonempty challenge
produces WWW-Authenticate with a 401; no challenge produces a 403. Unknown
synchronous header hooks and authenticator constructors run in the worker.
BaseAuthentication.authenticate_header() bridges async overrides for ordinary
synchronous DRF callers. Cancellation is propagated, not converted to a 403.
Conditional requests¶
get_etag and get_last_modified take the arguments of Django's
condition() callables (request, *args, **kwargs) and return the same
values: an ETag, quoted or not, a datetime (naive is read as UTC), or None.
Unlike Django's decorator, which calls them before the view and synchronously
even for an async view, aiodrf calls them after authentication, the view's permissions and
throttling, so a 304 or 412 is never an answer to a client those refuse.
Object permissions and the view's queryset scope have not been applied yet:
a validator that looks the object up must apply them itself, or a client
could learn whether an object it may not see exists, and its version. On a
generic view, aget_object() does (the queryset, filters, lookup and object
permissions), at the cost of reading the object once more in the handler:
class ArticleDetail(generics.RetrieveAPIView):
queryset = Article.objects.all() # or a tenant-scoped get_queryset()
serializer_class = ArticleSerializer
async def aget_etag(self, request, pk):
article = await self.aget_object() # 404 or 403 as the retrieve would
return f"{article.pk}-{article.version}"
Django's get_conditional_response decides (RFC 9110 section 13.2.2): a GET or
HEAD whose If-None-Match or If-Modified-Since matches gets a 304 and the
handler does not run; a failed If-Match or If-Unmodified-Since, or an
If-None-Match on a write, raises aiodrf.exceptions.PreconditionFailed, a
412 in DRF's error format (Django's decorator returns an empty 412). Safe
responses get ETag and Last-Modified unless the handler set them. Writes
without precondition headers do not call the hooks. Checking If-Match and
saving are two steps, as with Django's decorator: two writers can pass the
same check. Guard the write itself (a version column in the UPDATE's
WHERE) where that matters.
Query parameters¶
query_serializer_class (or get_query_serializer_class()) names a
serializer that validates request.query_params. Nothing validates it
automatically; a handler or get_queryset asks for the result:
class Filters(serializers.Serializer):
tag = serializers.ListField(child=serializers.CharField(), required=False)
limit = serializers.IntegerField(min_value=1, max_value=100, default=20)
class Articles(generics.ListAPIView):
query_serializer_class = Filters
def get_queryset(self): # runs in the worker with the rest of ``list``
params = self.get_validated_query_params()
queryset = Article.objects.all()
if "tag" in params:
queryset = queryset.filter(tags__name__in=params["tag"])
return queryset[: params["limit"]]
The result is the serializer's validated_data, computed once per request;
invalid input raises DRF's ValidationError (400). The QueryDict is passed
as the serializer's data, so DRF's HTML-input rules apply: a ListField
reads every value of a repeated key (?tag=a&tag=b) and an absent
BooleanField is False, not missing. A declarative serializer validates on
the loop; a project's get_query_serializer_class(), or a serializer with
code of its own, is built and validated in one worker hop. With
drf-spectacular, aiodrf.contrib.spectacular.AutoSchema documents the fields
as query parameters (ecosystem guide, section 2).
Finalizing a response¶
from typing import Any
from django.http import HttpResponseBase
from aiodrf.request import Request
from aiodrf.views import APIView
class TaggedView(APIView):
async def afinalize_response(
self,
request: Request,
response: HttpResponseBase,
*args: Any,
**kwargs: Any,
) -> HttpResponseBase:
response = await super().afinalize_response(request, response, *args, **kwargs)
response["X-API-Version"] = "1"
return response
An existing synchronous finalize_response override needs no rewrite. DRF's
negotiation, renderer context, Vary and response-header semantics remain in
the default finalizer. Custom synchronous get_renderer_context() runs in a
worker; the framework default does not add that hop. Finalization also applies
to responses returned by the exception handler. An exception in finalization
itself propagates, as in DRF; it is not recursively sent through finalization.
See DRF's APIView contract.
An aiodrf Response whose renderer can render on the loop has a render
that is marked as a coroutine function (implementation guide, section 5): awaiting
it renders inline or in one worker hop. Synchronous callers that are not on
the event loop (Django's sync handler, the test client, cache_page) get the
rendered response back directly. Code that calls response.render()
synchronously on the event loop thread, for example an async middleware,
should check inspect.iscoroutinefunction(response.render) and await
it; otherwise, for data that needs the worker, it receives an awaitable.
Policy and serializer hooks¶
Authentication supports authenticate / aauthenticate; permissions support
has_permission / ahas_permission and has_object_permission /
ahas_object_permission; throttles support allow_request / aallow_request.
Filter and pagination hooks have their corresponding a... dispatchers.
Keep policy constructors and wait() synchronous. Authentication challenges
also support aauthenticate_header().
A throttle's wait() is its own decision: an aallow_request() does not
make it safe on the event loop, so a wait() the project wrote runs in a
worker unless it is declared pure.
aiodrf serializers expose ais_valid(), asave() and adata(). Their
validation, representation and persistence extension points are described in
the implementation guide. Plain DRF serializers remain
usable through aiodrf.aio; they do not acquire these instance methods.
Views must use await request.adata() when parsing may perform I/O, and
await request.auser() / await request.aauth() for lazy authentication.
Hooks that remain synchronous¶
Keep these synchronous:
- View factories and context builders:
initialize_request,get_parsers,get_authenticators,get_renderers,get_permissions,get_throttles,get_query_serializer,get_query_serializer_classand query context builders. - Content negotiation and version selection, exception handler selection/context and success headers.
permission_deniedandthrottled, which DRF calls to raise. Anasync defwould return a coroutine nobody awaits and let the request through; put asynchronous auditing in a permission's or throttle's async hook.- Parser
parse, rendererrender, and metadatadetermine_metadata,determine_actions,get_serializer_info,get_field_info.
These callbacks can contain synchronous project code behind their existing
worker boundary; the metadata constructor and its computation, for example,
run together in one worker. Use the supported aget_serializer* counterparts
for async factory work, with schema-time defaults where needed.
APIView.as_view() validates known synchronous hooks and statically configured
components without constructing them. A detectable coroutine in those hooks
raises ImproperlyConfigured with the class and method name. It also resolves
the static serializer: a bare msgspec Struct or pydantic model is wrapped, and
a kind that ALLOWED_SERIALIZER_BACKENDS excludes is refused
(msgspec and pydantic guide). The same applies
to aiodrf viewsets through their cooperative as_view() chain. Parsers,
renderers and metadata selected dynamically are also checked at their execution
boundary. The internal hook registry is not an application extension API.
A decorator that wraps an async def method with a synchronous function
(functools.wraps) returns a coroutine, but its own code runs before that. The
dispatcher calls such a handler, @api_view function, exception handler or
a... pair member in a worker and awaits the coroutine on the event loop; so
do aiodrf.aio and aiodrf's serializers for validators, validate_<field> /
validate hooks and their a... members, SerializerMethodField methods and
serializer pair members. A model
method used as a source is called by DRF's get_attribute, in the worker
under REPRESENTATION_MODE = "thread". A wrapper that only creates
the coroutine can say so with asgiref.sync.markcoroutinefunction (or
inspect.markcoroutinefunction); it is then called on the event loop.
This is a diagnostic, not whole-program verification. Arbitrary descriptors,
unannotated synchronous wrappers returning awaitables, runtime monkeypatches
and nested hooks in third-party libraries cannot all be classified statically.
Registration does not prove an apparently async method is non-blocking. Keep
the library's conservative worker defaults; declare @async_safe only when
the project's entire synchronous call path is known not to block.