Как обнаружить отключение клиента в StreamingHttpResponse (события на стороне сервера) с помощью Django/Django-Ninja?

Мы хотим добавить события на стороне сервера в наше приложение, но в настоящее время цикл while True выполняется бесконечно, и отключение клиента не приводит к остановке задачи.

Воспроизводимый пример:

@api.get("/events")
async def get_events(request: ASGIRequest) -> StreamingHttpResponse:
    async def generate_message() -> AsyncGenerator[str, None]:
        while True:
            await asyncio.sleep(1)
            yield 'data: {"test": "testvalue"}\n\n'
            print("Working...")

    return StreamingHttpResponse(
        generate_message(),
        content_type="text/event-stream",
        headers={
            "X-Accel-Buffering": "no",
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        },
    )

Приведенный выше пример будет печатать Working... до тех пор, пока сервер не будет закрыт (даже если клиент закрыл соединение и не получает данные).

Есть ли способ обнаружить отключение клиента, чтобы остановить цикл while True?

Уже пытался вернуть простой HttpResponse, помещая код в try...except, создавая методы def close, async def close на StreamingHttpResponse.

  • django-ninja==0.22.2
  • django==4.2.13
  • uvicorn[standard]==0.29.0

Таким образом, обработка отключения клиента должна происходить автоматически, к сожалению, Django < 5 имеет ошибку в ASGIHandler (https://code.djangoproject.com/ticket/34752)

В версии Django 5.0.6 эта проблема исправлена.

изменяет ваш код для обработки отключений клиента .

i. Создайте промежуточное ПО ASGI для обнаружения разъединений.

в файле middleware.py

from typing import Callable

class SSEDisconnectMiddleware:
    def __init__(self, app: Callable):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope['type'] == 'http':
            async def receive_wrapper():
                message = await receive()
                if message['type'] == 'http.disconnect':
                    scope['client_disconnected'] = True
                return message

            scope['client_disconnected'] = False
            await self.app(scope, receive_wrapper, send)
        else:
            await self.app(scope, receive, send)

Добавьте это промежуточное ПО в ваше ASGI-приложение в asgi.py

import os
from django.core.asgi import get_asgi_application
from .middleware import SSEDisconnectMiddleware

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

application = SSEDisconnectMiddleware(get_asgi_application())

после этого,

ii. Измените генератор событий так, чтобы он учитывал отключения.

в файле views.py

from django.http import StreamingHttpResponse
from django_ninja import NinjaAPI
import asyncio
from typing import AsyncGenerator

api = NinjaAPI()

@api.get("/events")
async def get_events(request) -> StreamingHttpResponse:
    async def generate_message() -> AsyncGenerator[str, None]:
        while True:
            await asyncio.sleep(1)
            if hasattr(request, 'scope') and request.scope.get('client_disconnected'):
                print("Client disconnected.")
                break
            yield 'data: {"test": "testvalue"}\n\n'
            print("Working...")

    return StreamingHttpResponse(
        generate_message(),
        content_type="text/event-stream",
        headers={
            "X-Accel-Buffering": "no",
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        },
    )

Эта настройка гарантирует, что ваш генератор событий остановится, когда клиент отключится.

Вернуться на верх