Skip to content

Starlette 0.13.3 hangs with StreamingResponse & httpx client #908

@JayH5

Description

@JayH5

When using a StreamingResponse and, by extension, any custom middleware (any subclass of BaseHTTPMiddleware) with Starlette 0.13.3, with the async httpx client directly connected to the app (i.e. httpx.AsyncClient(app=...)), Starlette responses never complete. This did not happen with Starlette 0.13.2.

Minimal-ish test case:
With starlette==0.13.3, httpx==0.12.1, and pytest + pytest-asyncio:

import httpx
import pytest
from starlette.applications import Starlette
from starlette.responses import StreamingResponse
from starlette.routing import Route


@pytest.fixture
def app():
    def data():
        yield b"data"

    async def stream(request):
        return StreamingResponse(data())

    return Starlette(routes=[Route("/stream", stream)])


@pytest.fixture
async def client(app):
    async with httpx.AsyncClient(app=app) as client:
        yield client


@pytest.mark.asyncio
async def test_middleware(client):
    response = await client.get("http://example.com/stream")
    assert response.status_code == 200

Expected behaviour:
The test passes

Actual behaviour:
The await client.get() call never returns/completes.

Stacktrace:
If you hit ctrl-c while the test is stuck you get a long stacktrace. But the important bits I believe are:

File "/Users/jamie/.virtualenvs/tempenv-51153646622a/lib/python3.7/site-packages/starlette/responses.py", line 202, in listen_for_disconnect
    message = await receive()
  File "/Users/jamie/.virtualenvs/tempenv-51153646622a/lib/python3.7/site-packages/httpx/_dispatch/asgi.py", line 78, in receive
    body = await request_body_chunks.__anext__()

Suspected cause:
Starlette calls both the receive() (to check for disconnects) and send() (to send the response) concurrently in its StreamingResponse type (since #839). It tries to wait for either of those two options to complete (using asyncio.wait({tasks}, return_when=asyncio.FIRST_COMPLETED)). Because the ASGI dispatch in httpx is generally effectively synchronous, both send() and receive() may never return control to the event loop when called which means that if either task that is waited on does not complete, then asyncio.wait() will not return and the program will remain stuck in an infinite loop inside the StreamingResponse.listen_for_disconnect method.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions