Skip to content

[BUG] SSE async generators trigger "synchronous iterators" warning under ASGI #1714

@adonig

Description

@adonig

Description

When using django-ninja's SSE streaming with an async generator under ASGI (granian/uvicorn), Django emits this warning on every SSE connection:

StreamingHttpResponse must consume synchronous iterators in order to serve them
asynchronously. Use an asynchronous iterator instead.

The warning is cosmetic — SSE streaming works correctly via Django's sync-to-async fallback — but it indicates that the async generator is not being detected as async by Django's StreamingHttpResponse.

Root Cause

In ninja/compatibility/streaming.py, the create_streaming_response function (Django 4.2+ path) wraps the async content generator in another async generator (with_lazy_headers) and passes it to StreamingHttpResponse:

async def with_lazy_headers() -> Any:
    async for chunk in content_gen:
        yield chunk
    _copy_temporal_headers(temporal_response, response)

response = StreamingHttpResponse(
    with_lazy_headers(),
    content_type=content_type,
    status=status,
)

Django's StreamingHttpResponse._set_streaming_content then does:

def _set_streaming_content(self, value):
    try:
        self._iterator = iter(value)      # ← succeeds on async generators!
        self.is_async = False              # ← wrongly set to False
    except TypeError:
        self._iterator = aiter(value)
        self.is_async = True

The issue: in Python 3.12+, iter() on an async generator does not raise TypeError. It returns a (useless) iterator wrapper. So Django's detection logic fails — it classifies the async generator as synchronous.

When Django's ASGI handler later tries to iterate via __aiter__, it catches the TypeError from the sync iterator, logs the warning, and falls back to sync_to_async(list) consumption — which defeats the streaming purpose.

Environment

  • django-ninja 1.6
  • Django 6.0
  • Python 3.14
  • granian ASGI server

Reproduction

from ninja import NinjaAPI
from ninja.streaming import SSE
from collections.abc import AsyncIterator
import asyncio

api = NinjaAPI()

class Event:
    message: str

@api.get("/stream", response=SSE[Event])
async def stream(request) -> AsyncIterator[Event]:
    for i in range(5):
        yield Event(message=f"event {i}")
        await asyncio.sleep(1)

Run under any ASGI server and connect to /stream. The warning appears in the server logs for every connection.

Suggested Fix

In ninja/compatibility/streaming.py, check for async iterator first before trying iter():

from collections.abc import AsyncIterator

# In create_streaming_response, before passing to StreamingHttpResponse:
if isinstance(content_gen, AsyncIterator):
    # Wrap in a class that only exposes __aiter__, not __iter__,
    # so Django's _set_streaming_content detects it correctly.
    class _AsyncOnly:
        def __init__(self, ait):
            self._ait = ait
        def __aiter__(self):
            return self._ait.__aiter__()

    response = StreamingHttpResponse(
        _AsyncOnly(with_lazy_headers()),
        content_type=content_type,
        status=status,
    )

Alternatively, this could be fixed in Django's _set_streaming_content by checking isinstance(value, AsyncIterator) before iter(). But since django-ninja controls the intermediary, a fix here is more practical.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions