Third-party packages¶
This page lists the Django and DRF packages tested with aiodrf, what was tested and what to watch for. Each package runs against aiodrf views and, where DRF has an equivalent, is compared with the same view written for DRF, through both synchronous and asynchronous requests. A row covers the scenarios it names, not every feature of the package. The tested versions are listed in the version reference; setup instructions and links to each package's documentation are in the optional dependency reference.
In the table, a hop is one switch from the event loop to a worker thread to run synchronous code; "one hop" means the whole action ran in a single switch.
Tested packages¶
The native async cache backends are tested separately with django-valkey 0.4.1
and valkey-py 6.1.1, and with redis-py's redis.asyncio client, including
compatibility with the formats of Django's Redis backend and django-redis.
These tests cover connection pool ownership, ASGI state, errors, batches,
expiry, concurrent counters, the page-cache middleware, cancellation and
shutdown, plus Sentinel discovery, a controlled failover and cross-slot Cluster
operations. They do not cover every high-availability topology or plugin. See
native async cache and NoSQL databases,
which also explain the difference between Django's awaitable ORM and native
async drivers.
| Package | Tested behaviour |
|---|---|
| drf-spectacular | schema parity, aliases, name collisions, recursive graphs, PATCH, live payload validation and explicit SSE/NDJSON item schemas |
| django-filter | filters, a FilterSet.filter_queryset that queries |
| rest-filters | typed query validation, relation validation, unknown parameters, authorized queryset scope, request isolation and spectacular schema parity |
| django-async-backend | Version 6.1.5. aiodrf.contrib.async_backend on PostgreSQL, under ASGI and WSGI: pages, filters, retrieve, create and update with to-many fields and their signals, cascading delete, rollback, sync callers, sessions left open, hops (guide) |
| djangorestframework-simplejwt | token from simplejwt's view authenticates; invalid, expired, foreign and missing tokens; hops |
| django-rest-knox | token authentication, revocation, malformed headers; hops |
| django-oauth-toolkit | bearer tokens, expired tokens, TokenHasScope and its required_scopes message |
| drf-auth-kit | JWTCookieAuthentication and TokenCookieAuthentication with aiodrf.contrib.auth_kit: token in the header, in the cookie, in both, cookie profiles by Origin, malformed, invalid and foreign tokens answer as DRF; no hop without a token, one with |
| dj-rest-auth | the JWT cookie of its login view authenticates aiodrf views |
| django-guardian, djangorestframework-guardian | ObjectPermissionsFilter, DjangoObjectPermissions with the view permission; the same answers with aiodrf.contrib.permissions, which asks guardian through has_perms |
| drf-nested-routers | nested list, retrieve, create; a child under the wrong parent is 404 |
| drf-standardized-errors | 400, 401, 404, 405 and unexpected errors as EXCEPTION_HANDLER |
| django-cors-headers | response headers and preflight requests; QUERY needs CORS_ALLOW_METHODS |
| django-simple-history | history_user of create, update, delete made in aiodrf's worker thread |
| django-axes | lockout after failed BasicAuthentication logins |
| drf-orjson-renderer | renderer and parser produce DRF's bytes |
| drf-excel | the workbook equals the one of the DRF view |
| django-cachalot | lists and retrieves served from the cache (proved with a write cachalot cannot see) and invalidated by writes from worker threads; a save rolled back after an in-transaction read leaves the cache correct; hops unchanged |
| django-cacheops | list, retrieve and Django's aget/acount in an async def handler served from Redis; create, update and delete invalidate; a save rolled back after an in-transaction read leaves neither the read nor the invalidation behind; hops unchanged |
| django-cleanup | update deletes the replaced file, destroy deletes the file, both after the commit and off the event loop; a rolled-back update keeps the old file (the new upload stays, as with DRF); hops unchanged |
| django-elasticsearch-dsl | _bulk indexing from the worker thread on create, update and delete; a search in get_queryset runs in the hop; native elasticsearch.dsl.AsyncSearch; the synchronous client in an async handler blocks the loop; Elasticsearch-only, not an OpenSearch adapter |
| django-opensearch-dsl, opensearch-py | Public document hooks in workers, native index/delete/bulk/search, deferred fields, errors and cancellation; optional live 2.x/3.x mapping/aggregation/pagination |
| django-typer | a TyperCommand whose handler is async def, run by aiodrf.management.AsyncCommand (guide); without it, django-typer returns the coroutine unawaited |
| django-redis, redis-py | throttling and cache_page, as for every cache backend |
| django-zeal | sees the queries of aiodrf's worker thread; Meta.auto_prefetch leaves none to report |
| asgi-lifespan, HTTPX | real ASGI root with typed lifespan state via manager.app, HTTP dispatch and cleanup |
channels, djangochannelsrestframework (+ daphne for channels.testing) |
an aiodrf permission with only ahas_permission, alone and in IsAuthenticated \| ..., refuses or allows a WebSocket connection and its actions; actions awaiting aiodrf serializers (ais_valid/asave/adata, aiodrf.aio for DRF serializers, an async validator) answer create, patch, list and validation errors exactly as database_sync_to_async actions do; hops per action |
| django-health-check | database and cache checks through aiodrf.asgi.get_asgi_application() next to aiodrf views; a failing check is a 500 |
| django-idempotency-key | IdempotencyKeyMiddleware: a repeated key replays the first response (409) without a second write; a missing key is a 400; the create still costs one hop |
| djoser | users registered through its endpoints; its token login and JWT create authenticate aiodrf views; token revocation; one hop |
| django-allauth (headless) | app X-Session-Token (allauth's XSessionTokenAuthentication) and browser session logins authenticate aiodrf views; app logout; one hop |
| rules | AutoPermissionViewSetMixin with model rule permissions: view, change, delete, add; its synchronous initial() override runs in one extra hop. The synchronous rule backend grants permissions through aiodrf.contrib.permissions and DRF's class; do not assume Django's async permission hook invokes a backend that implements only the synchronous hook. |
| drf-api-logger | its async-capable middleware logs the same body, response and status for DRF and aiodrf views, masking included; under ASGI its signal runs on the event loop |
| apitally | its sync-only middleware counts aiodrf views like DRF views; DRF's endpoint introspection finds them; under ASGI Django runs the middleware in a thread, aiodrf adds no hop |
| wireup | constructor injection into class-based views: its synchronous replacement callback keeps the aiodrf view's coroutine marker, so Django awaits dispatch(); no hop |
| django-money | MoneyField amount and currency round-trip, model default currency, validation errors; a PATCH of the amount alone takes the default currency, as in DRF |
| django-phonenumber-field (phonenumberslite) | E164 storage, invalid numbers, PHONENUMBER_DEFAULT_REGION read per request; hops |
| django-taggit | TaggitSerializer create and update set the tags in the create's hop; Meta.auto_prefetch loads the tags like prefetch_related; TagList is rendered in a thread |
| drf-extra-fields (Pillow) | Base64ImageField decode and PresentablePrimaryKeyRelatedField representation run in the worker thread, never on the loop; errors as in DRF |
| django-pydantic-field (pydantic) | serializer SchemaField, SchemaParser[T]/SchemaRenderer[T] byte parity, its OpenAPI AutoSchema; the generic aliases cost three hops |
| drf-writable-nested | nested FK, reverse-FK and M2M create/update and 400s as in DRF; UniqueFieldsMixin; a child refused in the save keeps the parent, as in DRF (its own save() override is not wrapped by ATOMIC_SAVE); an atomic perform_create rolls back; one hop |
| drf-flex-fields | expand, fields, omit on list and retrieve with DRF's queries; Meta.auto_prefetch sees the fields before expansion; FlexFieldsFilterBackend does not import on DRF 3.18 (coreapi); use FlexFieldsMixin with aiodrf's ModelViewSet, not FlexFieldsModelViewSet; one hop |
| django-polymorphic, django-rest-polymorphic | PolymorphicSerializer list, retrieve, create, update and resourcetype errors per subclass; one hop |
| django-restql | ?query= field selection, arguments as filters through its sync dispatch, eager loading, nested create/update; ATOMIC_SAVE rolls back a nested create refused late; one hop |
| djangorestframework-dataclasses | DataclassSerializer errors and ORM-less save() via aiodrf.aio in an APIView and in generic list/create; one hop |
| djangorestframework-jsonapi | renderer, parser, pagination, QueryParameterValidationFilter/OrderingFilter/DjangoFilterBackend, metadata, exception handler, ?include=; AutoPrefetchMixin/PreloadIncludesMixin on an aiodrf ModelViewSet (DRF's bytes and queries, one hop) |
| djangorestframework-camel-case | key round-trip, errors and CamelCaseMiddleWare as in DRF; the unknown parser costs one hop and the renderer runs in Django's thread; neither when declared in PURE_POLICIES/INLINE_RENDERERS |
| nested-multipart-parser | nested multipart with a file, request.data/request.FILES as in DRF (with DRF_NESTED_MULTIPART_PARSER={"querydict": False}), one hop |
| drf-tweaks | ApiVersionMixin with DeprecationMiddleware, AutoOptimizeMixin (DRF's SQL), @autofilter, NoCountsLimitOffsetPagination (its OrderedDict page renders on the loop, like a dict); one hop |
| djangorestframework-datatables | renderer, filter backend and paginator sharing counts on the view in one hop; DRF's bytes |
| drf-restwind | Browsable API templates: list and detail pages with forms, same HTML as DRF |
| servestatic | ASGI static delivery: GET/HEAD, ETag, ranges, gzip, missing/parent paths and API forwarding; no middleware patch |
| whitenoise | Original sync middleware and opt-in dual-mode adapter; ASGI synchronous-file buffering remains explicit. HTTP file semantics, worker lookup and cancellation cleanup |
| django-silk | request, response and worker-thread queries recorded as in DRF (not INSERTs, in DRF too); its sync middleware adds one crossing each way, no aiodrf hop |
| django-debug-toolbar | toolbar in Browsable API pages, the SQL panel sees worker-thread queries, history panel, streaming untouched; profiler off under ASGI; no aiodrf hop |
| django-structlog | request id and user bound by RequestMiddleware in logs from worker-thread hooks and async handlers; request_failed for unhandled errors; concurrent requests keep their own context; no aiodrf hop |
| django-log-request-id | the request id of its synchronous middleware in logs from worker-thread hooks and async handlers, request.id and the response header; concurrent requests keep their own id |
| django-tenants | per-domain schema for lists, retrieve, create and the async ORM; concurrent requests to different tenants through Django's ASGI handler on PostgreSQL; requests sharing one thread (Django's in-process async test client) share the schema, as for any Django async view |
| django-mongodb-backend, django-mongodb-extensions (PyMongo, MongoDB) | Version 6.1.0 on Django 6.1 and 5.2.4 on Django 5.2, 0.3.0 (4.18.2, 8.2.12). MongoModelSerializer alone and with aiodrf's ModelSerializer: CRUD, pages, django-filter, ObjectId keys in URLs and bodies, 404s and 400s as in DRF, one hop; embedded models and arrays; relations with aiodrf.contrib.mongodb's ObjectIdPrimaryKeyRelatedField or pk_field; ATOMIC_SAVE in MongoDB's transaction through aiodrf.contrib.mongodb, Django's no-op on a standalone server, aiodrf.W007; Meta.auto_prefetch without many-to-many prefetches; compiler declines; the async ORM with no hop (guide) |
| django-safedelete | destroy hides the row (SOFT_DELETE_CASCADE), retrieve and list stop serving it, a foreign key to it is refused, undelete() serves it again, as DRF's views answer; not with aiodrf.contrib.async_backend, whose async_delete() does not call the model's delete() |
| django-auditlog | two users' concurrent changes each attributed to their session user, masked fields, no entry for a rolled-back change; a user DRF authenticates in the view (JWT, token) is not the actor unless the view sets it (below) |
| django-prometheus | Version 2.5.0, on Django 6.0 (it declares Django < 6.1). Responses by status, view name and method; exceptions that escape DRF's handler; the latency ends when a streamed body starts; QUERY is <invalid method> in its response counters |
| Schemathesis | Version 4.28.0, a testing tool. Requests generated from the bookshop example's schema, anonymous and authenticated, cause no server error, and responses match their documented schema |
djongo is not supported: version 1.3.7 declares Django 2.1 to 3.1.12, and with
Django 5.2 or 6.1 every insert fails with SQLDecodeError. See the
MongoDB guide.
Caches, search indexes and middleware¶
- django-cacheops invalidates through signals, one transaction at a time.
aiodrf's save, its
transaction.atomicandon_commitrun in one worker hop, so invalidation happens at the commit, as with DRF.QuerySet.update()does not invalidate, as upstream documents (invalidated_update()). Its classifiers stop at Django 5.2 and Python 3.13, but it works with Django 6.1 and Python 3.14. Withaiodrf.contrib.async_backend, native reads (async_objects) are not cached, and native writes invalidate after the native commit (async backend guide). -
django-elasticsearch-dsl's signal processor calls the synchronous client, which is correct in aiodrf's worker thread. In an
async defhandler, useelasticsearch.dsl.AsyncSearchwithasync_connections(pip install "elasticsearch[async]"), and create and close the async client per event loop. Indexing happens onpost_save, so a rolled-back save is indexed anyway, as under DRF. With elasticsearch-py 9.4 or later, django-elasticsearch-dsl 9.0 indexes empty documents; work around it by overriding the document'sprepare()to return{name: prepare(instance) for name, _, prepare in self.init_prepare()}. django-elasticsearch-dsl-drf 0.22.5 cannot be used: it importsdistutils, removed in Python 3.12. -
djangochannelsrestframework 1.3.0 wraps DRF permission classes and calls their synchronous
has_permissionin a thread with a DjangoHttpRequestbuilt from the scope, not a DRFRequest; aiodrf's bridge runsahas_permissionfrom there. It callsasyncio.iscoroutinefunction, which emits a deprecation warning on Python 3.14. - django-health-check's view is a plain Django async view; synchronous checks run in the event loop's default executor, not in aiodrf's hops.
- django-idempotency-key's middleware is synchronous, so Django runs it in a
thread around the async view. Its default in-memory storage is per process
and replays the stored response object. Its lock covers the lookup of a
stored response, not the request, so two concurrent requests with one key
can both run. Use a database constraint where a duplicate write must be
impossible.
djangorestframework-idempotency-key 1.0.3 cannot be used: its decorator
requires the handler to return a
Response, so it cannot wrap anasync defhandler.
Audit attribution, metrics and deletion¶
- django-auditlog reads
request.userin its middleware, before DRF authenticates. A session user is there; a JWT or token user is not, and those changes have no actor. Set it where DRF has authenticated:
```python from auditlog.context import set_actor
class Notes(viewsets.ModelViewSet): async def aperform_create(self, serializer): with set_actor(self.request.user): await super().aperform_create(serializer) ```
aiodrf runs the save in a worker with the request's context, so the actor
reaches auditlog's signal receivers there.
- django-prometheus times a request until its response starts: the time
spent streaming a body (NDJSON, SSE) is not in
django_http_requests_latency_seconds_by_view_method. With several
processes (Uvicorn workers), each has its own registry; export them as
its documentation describes for multiprocess servers.
- django-safedelete works with aiodrf's generic views as with DRF's. The
native contrib deletes with async_delete(), which does not call a model's
delete(): soft-deleting models are not supported there.
Synchronous hooks of untested packages run in the request's worker thread with Django's context, including the active language, time zone and context variables. This lets most DRF packages work unchanged, but it does not make their I/O asynchronous, and the number of thread switches depends on which hooks a package overrides. Test the packages your project uses with its own views.
Channels and djangochannelsrestframework¶
A DCRF action is an async def on the consumer. It can await aiodrf
serializers instead of wrapping DRF code in database_sync_to_async:
from djangochannelsrestframework.decorators import action
from djangochannelsrestframework.generics import GenericAsyncAPIConsumer
from aiodrf import aio
class AuthorConsumer(GenericAsyncAPIConsumer):
queryset = Author.objects.order_by("name")
@action()
async def create(self, data, **kwargs):
serializer = AuthorSerializer(data=data) # an aiodrf serializer
await serializer.ais_valid(raise_exception=True)
await serializer.asave()
return await serializer.adata(), 201
@action()
async def list(self, **kwargs):
serializer = PlainAuthorSerializer(self.get_queryset(), many=True) # DRF's
return await aio.data(serializer), 200
A ValidationError raised by ais_valid(raise_exception=True) reaches the
client as DCRF formats any APIException: {"errors": [{"name": [...]}],
"response_status": 400}. Payloads, statuses and error bodies are the same as
those of the action written with database_sync_to_async.
Each awaited step that runs DRF code is one hop. A create requires
three (validation, the save, the representation), a refused create one, a list
one; the same action inside one database_sync_to_async call crosses to a
thread once. Awaiting the steps pays off when the serializer has async members
(validators, validate_<field>, acreate, async SerializerMethodFields),
which run on the event loop between the hops. For a serializer that is
synchronous throughout, one database_sync_to_async around all of it is
cheaper.
Pitfalls:
- Database connections. A consumer lives as long as its WebSocket. Django
closes connections at the end of an HTTP request, not of a WebSocket
message;
database_sync_to_asynccloses old connections around each call, aiodrf's hops run in asgiref's thread-sensitive executor without doing so. KeepCONN_MAX_AGE = 0(or a pool) and do not hold a transaction open across awaits. database_sync_to_asyncand aiodrf'srun_syncare both thread-sensitivesync_to_async; use DCRF's for code you write in the consumer, and let aiodrf run what the serializer needs.- After actions that awaited aiodrf's hops or the async ORM, call Channels'
await aclose_old_connections()(channels.db), for example at the end of the action or indisconnect(), asdatabase_sync_to_asyncwould have.
Error tracking, telemetry, Celery and S3¶
These packages were tested with aiodrf; aiodrf adds no adapter or dependency for them.
| Package | Tested version | Tested behaviour and limits |
|---|---|---|
| sentry-sdk | 2.70.0 | Django integration under ASGI and WSGI, isolation of concurrent requests and users, errors raised in views and in worker threads, exactly one event per error, and no false error when a client disconnects. Tested with an in-memory transport. |
| OpenTelemetry SDK, Django and ASGI instrumentation | 1.44.0, 0.65b0 | Parent-child relationships of server, view and worker spans; route, status and error attributes; context isolation under ASGI and WSGI; exporter failure; sampling. In the tested Django instrumentation, a cancelled request does not end its server span; this also happens without aiodrf. |
| Celery, redis-py | 5.6.3, 6.4.0 | Publishing after commit and not after rollback, worker placement and context, cancellation (which does not stop a publication already running), a prefork worker, a Redis outage and recovery. Remote failover, delivery durability and exactly-once execution were not tested. |
| django-storages, boto3 | 1.14.6, 1.43.100 | The S3 backend with FileField uploads, signed URLs, reading, deleting, access-denied errors and temporary-file cleanup, and the refresh of expired credentials. Tested without network access to AWS; IAM, STS and large multipart uploads were not tested. |
Install error-tracking and telemetry SDKs before the ASGI or WSGI handler is created. Sentry patches Django itself; aiodrf does not patch anything for it. The OpenTelemetry Django instrumentation needs its optional ASGI dependency to record ASGI requests, and its middleware is synchronous, which adds thread switches. Follow the official instrumentation setup, avoid enabling both the ASGI and the Django server instrumentation, and keep exporter, privacy and sampling policies in your application.
aiodrf.contrib.opentelemetry.TracingMixin (extra opentelemetry, which needs
only the API package) adds spans for aiodrf's phases below the server span of
Django's instrumentation: aiodrf.authenticate, aiodrf.check_permissions,
aiodrf.check_throttles, aiodrf.handler and aiodrf.finalize. They carry the
view class and action as attributes and nothing from the request. Your own spans
in the handler, including those opened in a worker thread, are children of
aiodrf.handler. Expected DRF responses (a 403, a 429) leave the span status
unset and record the exception name; other exceptions set it to error; a
cancelled handler ends its span with aiodrf.cancelled. Validation,
representation and saving happen inside the handler span; rendering happens
after the view, in Django's handler, and has no span of its own. The mixin opens
no spans until your application configures a tracer provider.
Celery publishes synchronously. Call it from a synchronous action hook, or from
an async hook through sync_to_async. Keep transactional work and its
transaction.on_commit() registration in one synchronous function. Do not use
asyncio.create_task() as a task queue, and do not retry a cancelled publish
automatically: its worker thread can still send the message.
OpenAPI and Swagger UI¶
Use drf-spectacular's generator and UI views. The schema is generated from the views and serializers; publishing it is the project's decision, and aiodrf adds no routes and changes no settings.
Settings:
INSTALLED_APPS = [
# Existing Django/project apps...
"rest_framework",
"drf_spectacular",
"aiodrf",
]
REST_FRAMEWORK = {
# Keep the application's auth, permission, renderer and filter settings.
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "Example API",
"VERSION": "1.0.0",
"COMPONENT_SPLIT_REQUEST": True,
}
URLs:
from django.urls import path
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
urlpatterns = [
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/docs/",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
path(
"api/redoc/",
SpectacularRedocView.as_view(url_name="schema"),
name="redoc",
),
# Existing API URLs...
]
Redoc is optional. Namespaced applications pass the namespaced schema URL name. Protect the schema and UI views with the application's permission policy; they are DRF views and need no conversion to aiodrf.
In a project, validate the generated schema with:
python manage.py spectacular --file /tmp/aiodrf-schema.yml --validate --fail-on-warn
(aiodrf's own repository has no manage.py; its tests use SchemaGenerator
and call_command("spectacular", ...) under the test settings.)
Offline schema and lifespan¶
Schema generation and Django system checks do not start the ASGI lifespan. If
get_serializer_class() or get_queryset() depends on a live resource, return
the swagger_fake_view fallback before touching it. Keep that selection
synchronous, and do not open an HTTP client or start background tasks in
AppConfig.ready() for schema generation.
This follows spectacular's schema-time guidance.
The generated schema is the same before, during and after a managed lifespan, and the schema and check commands never call the lifespan factory.
aiodrf schema extensions¶
aiodrf/contrib/spectacular/extensions.py is imported by AppConfig.ready()
when drf-spectacular is installed (only the package being absent is
tolerated; an error inside it is raised). It registers aiodrf's
SessionAuthentication, which spectacular matches by exact class, and
documents msgspec/pydantic serializers from their schema classes: the input
schema for requests (an explicit Meta.partial_schema for PATCH), the output
schema for responses, with the names the runtime uses (by_alias=True for
pydantic). Components are identified by their shape. A pydantic model whose
validation and serialization shapes differ (a serialization_alias) gets a
<Name>Request component for the request side, nested or top-level; without
COMPONENT_SPLIT_REQUEST spectacular would not split it, so aiodrf does and
warns. DRF-defined serializers are documented by spectacular as usual,
whichever backend produces their output.
The generated OpenAPI 3.1 document validates, and real nested POST and
partial PATCH bodies validate against its components. Recursive Pydantic
roots resolve to a component definition with both older (2.7) and current
Pydantic versions.
Query serializers (query_serializer_class, see the
extension-hook contract) are
documented as query parameters by aiodrf.contrib.spectacular.AutoSchema,
per action where get_query_serializer_class() depends on it:
REST_FRAMEWORK = {"DEFAULT_SCHEMA_CLASS": "aiodrf.contrib.spectacular.AutoSchema"}
Without it, @extend_schema(parameters=[Filters]) documents the same fields.
Views with a QUERY handler make drf-spectacular 0.30 fail: it builds a
request for every method with DRF's APIRequestFactory, which has no
query(), and OpenAPI 3.0/3.1 have no QUERY operation. Leave those
operations out with the preprocessing hook:
SPECTACULAR_SETTINGS = {
"PREPROCESSING_HOOKS": [
"aiodrf.contrib.spectacular.hooks.preprocess_exclude_query_method"
],
}
For SSE and NDJSON, use the StreamSchema(ItemSerializer) annotation from
aiodrf.contrib.spectacular. It resolves item components without executing the
stream and does not describe the body as a JSON array. The
streaming schema guide has its usage and the limits of
its x-aiodrf-item-schema extension, which generic OpenAPI clients may ignore.
django-filter¶
Use django_filters.rest_framework.DjangoFilterBackend;
aiodrf.contrib.django_filters re-exports it for older imports. The whole
backend (the FilterSet, its form, filter_queryset, custom methods) runs
inside the action's hop, like every filter backend that is not declared pure.
DRF's SearchFilter and OrderingFilter run there as well: they call
get_search_fields() and build the view's serializer, which a project may
have written with queries.
rest-filters¶
Use the vendor's rest_filters.FilterBackend and FilterSet directly. The
backend implements DRF's synchronous filtering protocol, so aiodrf runs it in
the existing worker boundary; no contrib adapter or monkeypatch is needed.
Serializer fields validate parameters before queryset filtering. Queryset scope
set by the view is retained. The
data example exposes an explicitly
filtered list/detail route alongside the other serializer integrations.
The ecosystem tests compare DRF and aiodrf through both Django test clients and verify drf-spectacular parameter names/types. The vendor's schema implementation uses spectacular internals, so schema coverage is version-specific. Nested filter groups, arbitrary application overrides and every vendor constraint combination are not certified by these tests. See rest-filters for its complete API.
Authentication¶
DRF's authentication order and its first-success rule are kept. Session
authentication uses Django's async accessor for a lazy user and respects a
concrete request.user set by middleware, as DRF does. Its CSRF check is inline
unless the secret is in the session or the body is multipart. Header-based classes run
in one hop, and in none for a request that carries nothing for them:
INSTALLED_APPS += ["aiodrf.contrib.simplejwt", "aiodrf.contrib.knox"]
Both apps only call aiodrf.authentication.register_credentials_check, which
any project can use for its own header-based class. The check applies while
authenticate() is the one the registered class defines; a subclass that
overrides it (dj-rest-auth's JWTCookieAuthentication) is asked in a thread.
drf-auth-kit's JWTCookieAuthentication and TokenCookieAuthentication are
such subclasses: they read the token from the Authorization header or,
without one, from the cookie of the request's cookie profile
(AUTH_COOKIE_PROFILES, chosen by Origin). aiodrf.contrib.auth_kit
registers the same two steps as their check, so a request with neither costs
no hop, whatever AUTH_TYPE says:
INSTALLED_APPS += ["aiodrf.contrib.auth_kit"]
JWTStatelessUserAuthentication loads no user, but whether it does I/O
depends on settings (JWK_URL fetches keys, the blacklist app queries), so
aiodrf does not declare it pure; aiodrf/contrib/simplejwt/__init__.py shows
how a project does.
Caches¶
DRF's rate throttles run on the event loop when their cache is Django's
LocMemCache or DummyCache, and in a thread with every other backend,
including subclasses of those two that add code. LocMemCache is per
process: with several workers each has its own counters, in DRF as in
aiodrf.
DRF's throttles read the request history, append to it and write it back, so
concurrent requests can all pass (DRF's documentation says so).
aiodrf.throttling.FixedWindowRateThrottle, with AnonFixedWindowRateThrottle,
UserFixedWindowRateThrottle and ScopedFixedWindowRateThrottle, keeps DRF's
rates, scopes and cache keys and counts one number per window with the cache's
add and incr:
| Backend | incr |
Concurrent requests per window |
|---|---|---|
| LocMem | under a lock | exactly the rate, per process |
| Django's Redis, django-redis | atomic on the server | exactly the rate |
| Memcached (not in the test matrix) | atomic on the server | exactly the rate |
| Database, file | read and write | approximately the rate, as with DRF |
| Dummy | nothing is stored | not limited, as with DRF |
The window starts at a multiple of its duration, not at a client's first
request: a client can send the rate at the end of one window and again at the
start of the next. The counter's key names the window and expires a second
after it, so Django's Redis backend, whose incr is EXISTS then INCR,
cannot leave a counter without an expiry behind. With LocMem it runs on the
loop; with DummyCache, whose incr is BaseCache's, and with every remote
backend, it runs in one hop together with the other throttles.
Django's cache_page looks the response up with a synchronous cache.get()
before it awaits an async view. On the event loop that blocks for a round
trip to Redis or Memcached, and raises SynchronousOnlyOperation with the
database cache.
aiodrf.cache.cache_page is the same decorator with the lookup in a thread
unless the cache is in-process; keys, Vary and Cache-Control are
Django's. vary_on_headers, vary_on_cookie, cache_control and
never_cache do no I/O; use Django's. aiodrf.cache.cache_page and the
throttles are tested with LocMem, Dummy, database, file, Django's Redis backend
and django-redis.
Django features¶
aiodrf views are tested with every session engine, with and without
CSRF_USE_SESSIONS; with JSON bodies held in memory and spooled to disk,
multipart uploads, malformed JSON, unsupported media types and
DATA_UPLOAD_MAX_MEMORY_SIZE; with LocaleMiddleware translations and an
activated time zone inside the worker thread; behind a synchronous-only
middleware; with async streaming responses; and with database routers,
including the database ATOMIC_SAVE opens its transaction on.
ATOMIC_REQUESTS cannot be used with async views: Django raises an error, and
the system check aiodrf.W002 reports it at start-up.
Django's stock middleware works unchanged: security, common and frame-option
headers and redirects, messages with signed-cookie sessions, the session CSRF
token and origin and referer checks, RemoteUserMiddleware and
LoginRequiredMiddleware, GZipMiddleware with ConditionalGetMiddleware,
ETags and HEAD, and Server-Sent Events heartbeats, backpressure and
disconnects. Two limits come from Django itself:
- Django 5.2 can keep a stale anonymous
request.auser()result afteralogin(). aiodrf's session authentication uses the user the middleware set and is not affected. See Django #37042. - In Django 6.0, the async
GZipMiddlewarebuffers small chunks, so Server-Sent Events and heartbeats may be delayed. Django 5.2 and 6.1 are not affected. On 6.0, excludetext/event-streamfrom compression in your project or proxy, for example with aGZipMiddlewaresubclass that skips it. See Django #36293.
Custom middleware orders are not covered by these tests.
Django's task framework (Django 6.0 and later), and django-tasks 0.12 on
Django 5.2, work from aiodrf views as from Django's own async views: use
aenqueue() in async code. ImmediateBackend runs a synchronous task in the
request's thread and an async def task on the event loop. Calling the
synchronous enqueue() from a coroutine records Django's
SynchronousOnlyOperation as the task's failure. See Django Tasks.
Multipart uploads, including files Django spools to disk, are parsed and saved to storage within the action's hop.
All of these features are also tested on PostgreSQL, including what SQLite
cannot show: a failed statement inside ATOMIC_SAVE leaves the connection
usable, and on_commit callbacks, failing signal receivers and nested
atomic() blocks behave as in Django.