Другое

Django REST Framework: Почему async представления не работают

Решение ошибки AssertionError при использовании async представлений в Django REST Framework. Узнайте, как правильно настроить асинхронные API с adrf, sync_to_async и Django Ninja.

Почему Django REST Framework не распознает асинхронное представление?

Я получаю ошибку AssertionError: Expected a 'Response', 'HttpResponse' or 'StreamingHttpResponse' to be returned from the view, but received a class coroutine при использовании асинхронного представления в Django REST Framework.

Вот мой код:

views.py

python
@api_view(['POST'])
@permission_classes([AllowAny])
async def create_table(request):
    table_title = request.data.get['table_title']
    cols = request.data.get['cols'].split(';')
    rows = request.data.get['rows']
    async with pool.DB_POOL.acquire() as connection:
        create_statment = f'''CREATE TABLE IF NOT EXISTS {table_title}\n'''
        for col in cols:
            create_statment += f'{col} TEXT NOT NULL,\n'
        await connection.execute(create_statment)
    return Response(
        {'proccess': 'done'},
        status=status.HTTP_201_CREATED
    )

asgi.py

python
'''определение промежуточного слоя DBPoolMiddleware для инициализации и закрытия пула подключений в соответствии с состоянием сервера'''

import os
from django.core.asgi import get_asgi_application
import django
from . import pool

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

application = get_asgi_application()

class DBPoolMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope['type'] == 'lifespan':
            while True:
                message = await receive()
                if message['type'] == 'lifespan.startup':
                    django.setup()
                    await pool.create_db_pool()
                    await send({'type': 'lifespan.startup.complete'})
                elif message['type'] == 'lifespan.shutdown':
                    await pool.shutdown_pool()
                    await send({'type': 'lifespan.shutdown.complete'})
                    break
                else:
                    break
        else:
            await self.app(scope, receive, send)

application = DBPoolMiddleware(application)

settings.py

python
WSGI_APPLICATION = 'config.wsgi.application'
ASGI_APLICATION = 'config.asgi.aplication'

requirements.txt

asgiref==3.10.0
asyncpg==0.30.0
cffi==2.0.0
click==8.3.0
colorama==0.4.6
cryptography==46.0.3
Django==5.2.8
djangorestframework==3.16.1
h11==0.16.0
jwt==1.4.0
pycparser==2.23
sqlparse==0.5.3
tzdata==2025.2
uvicorn==0.38.0

Я запускаю сервер командой uvicorn config.asgi:application --host 0.0.0.0 --port 8000, после отправляю запрос через Postman и получаю ошибку.

Я думал, что современные версии Django должны поддерживать асинхронные представления без дополнительных декораторов.

P.S. Я знаю про инъекции, но пока просто экспериментирую.

Django REST Framework не распознает ваше асинхронное представление, потому что декоратор @api_view ожидает синхронную функцию, а не асинхронную. Даже в современных версиях Django и DRF требуется специальная обработка для асинхронных представлений.


Содержание


Основная проблема и почему она возникает

Ваша проблема заключается в том, что стандартный декоратор @api_view в Django REST Framework предназначен для работы с синхронными функциями. Когда вы используете асинхронную функцию со стандартным декоратором DRF, он пытается вызвать её как обычную функцию, но получает корутину вместо результата.

python
@api_view(['POST'])  # Этот декоратор ожидает синхронную функцию
async def create_table(request):  # Но вы передаете асинхронную
    # ...

Из документации Django видно, что Django поддерживает асинхронные представления, но DRF требует специальной обработки. Как объясняется в обсуждении на GitHub, для асинхронных представлений нужны либо специальные декораторы, либо использование сторонних решений.


Решение 1: Использование стороннего пакета adrf

Самый простой способ решить проблему — использовать пакет adrf, который предоставляет асинхронную поддержку для Django REST Framework.

bash
pip install adrf

Затем измените ваш код:

python
from adrf.decorators.api_view import api_view
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import status

@api_view(['POST'])
@permission_classes([AllowAny])
async def create_table(request):
    table_title = request.data.get('table_title')
    cols = request.data.get('cols').split(';')
    rows = request.data.get('rows')
    
    async with pool.DB_POOL.acquire() as connection:
        create_statement = f'''CREATE TABLE IF NOT EXISTS {table_title}\n'''
        for col in cols:
            create_statement += f'{col} TEXT NOT NULL,\n'
        await connection.execute(create_statement)
    
    return Response(
        {'process': 'done'},
        status=status.HTTP_201_CREATED
    )

Как показано в примере из документации adrf, для viewset все обработчики должны быть асинхронными. Пакет adrf полностью совместим с Django 5.2 и предоставляет ту же функциональность, что и стандартный DRF, но с поддержкой async/await.


Решение 2: Обертка sync_to_async

Если вы не хотите использовать сторонние пакеты, можете обернуть вашу асинхронную функцию с помощью sync_to_async:

python
from asgiref.sync import sync_to_async
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import status

@api_view(['POST'])
@permission_classes([AllowAny])
def create_table(request):
    # Создаем асинхронную обертку
    async def async_create():
        table_title = request.data.get('table_title')
        cols = request.data.get('cols').split(';')
        rows = request.data.get('rows')
        
        async with pool.DB_POOL.acquire() as connection:
            create_statement = f'''CREATE TABLE IF NOT EXISTS {table_title}\n'''
            for col in cols:
                create_statement += f'{col} TEXT NOT NULL,\n'
            await connection.execute(create_statement)
        
        return {'process': 'done'}
    
    # Выполняем асинхронную функцию в синхронном контексте
    return Response(
        sync_to_async(async_create)(),
        status=status.HTTP_201_CREATED
    )

Этот подход использует функцию sync_to_async, которая позволяет вызывать асинхронный код из синхронного контекста. Однако этот метод менее эффективен, так как блокирует выполнение во время ожидания завершения асинхронной операции.


Решение 3: Переход на Django Ninja

Для современных асинхронных API рекомендуется использовать Django Ninja, который изначально разработан для работы с async/await:

bash
pip install django-ninja

Пример использования:

python
from ninja import NinjaAPI
from ninja.security import APIKeyAuth

api = NinjaAPI()

@api.post("/create-table", auth=None)
async def create_table(request, table_title: str, cols: str, rows: int):
    cols_list = cols.split(';')
    
    async with pool.DB_POOL.acquire() as connection:
        create_statement = f'''CREATE TABLE IF NOT EXISTS {table_title}\n'''
        for col in cols_list:
            create_statement += f'{col} TEXT NOT NULL,\n'
        await connection.execute(create_statement)
    
    return {'process': 'done'}

Django Ninja полностью асинхронный и предоставляет современный синтаксис для API. Он идеально подходит для высоконагруженных асинхронных приложений.


Решение 4: Создание кастомного декоратора

Вы можете создать собственный декоратор, который будет правильно обрабатывать асинхронные функции:

python
from functools import wraps
from asyncio import iscoroutinefunction
from rest_framework.response import Response
from rest_framework.decorators import api_view as original_api_view

def async_api_view(http_method_names):
    def decorator(view_func):
        if iscoroutinefunction(view_func):
            @wraps(view_func)
            def wrapped_view(*args, **kwargs):
                # Запускаем асинхронную функцию и ожидаем результата
                coroutine = view_func(*args, **kwargs)
                import asyncio
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                try:
                    result = loop.run_until_complete(coroutine)
                    return result
                finally:
                    loop.close()
            return original_api_view(http_method_names)(wrapped_view)
        else:
            return original_api_view(http_method_names)(view_func)
    return decorator

Затем используйте его вместо стандартного декоратора:

python
@async_api_view(['POST'])
@permission_classes([AllowAny])
async def create_table(request):
    # ваш код

Настройка ASGI для производительности

Убедитесь, что ваша ASGI-конфигурация оптимизирована для асинхронной работы. В вашем asgi.py есть правильная настройка, но можно улучшить производительность:

python
import os
from django.core.asgi import get_asgi_application
import django
from . import pool

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

application = get_asgi_application()

class DBPoolMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope['type'] == 'lifespan':
            while True:
                message = await receive()
                if message['type'] == 'lifespan.startup':
                    django.setup()
                    await pool.create_db_pool()
                    await send({'type': 'lifespan.startup.complete'})
                elif message['type'] == 'lifespan.shutdown':
                    await pool.shutdown_pool()
                    await send({'type': 'lifespan.shutdown.complete'})
                    break
                else:
                    break
        else:
            # Добавляем обработку ошибок
            try:
                await self.app(scope, receive, send)
            except Exception as e:
                await send({
                    'type': 'http.response.start',
                    'status': 500,
                    'headers': [(b'content-type', b'application/json')],
                })
                await send({
                    'type': 'http.response.body',
                    'body': b'{"error": "Internal server error"}',
                })

application = DBPoolMiddleware(application)

Для запуска используйте uvicorn с правильными настройками:

bash
uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --workers 1 --loop asyncio

Важно использовать --workers 1 для асинхронного приложения, так как каждый воркер должен иметь свой event loop.


Заключение

Основная причина вашей проблемы — несовместимость стандартного декоратора @api_view с асинхронными функциями в Django REST Framework. Вот основные решения:

  1. Используйте adrf — самый простой способ добавить асинхронную поддержку в существующий DRF-проект
  2. Оберните с sync_to_async — подходит для простых случаев, но менее эффективно
  3. Перейдите на Django Ninja — современный фреймворк, изначально созданный для асинхронных API
  4. Создайте кастомный декоратор — для полного контроля над процессом

Для максимальной производительности в асинхронном режиме рекомендуется использовать Django Ninja или полностью перейти на adrf. В текущей версии вашего кода ошибка возникает потому, что DRF ожидает синхронный обработчик, а получает корутину.

Даже в Django 5.2 поддержка асинхронного ORM и других компонентов все еще находится в разработке, как упоминается в документации Django. Поэтому для полноценной асинхронной работы лучше использовать специализированные решения.

Источники

  1. Django asynchronous support documentation
  2. adrf - Async support for Django REST framework
  3. Async view support discussion in DRF GitHub
  4. Mastering Asynchronous Views in Django REST Framework
  5. Modernizing Django REST Framework with Asynchronous Views
Авторы
Проверено модерацией
Модерация