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.
When using a
StreamingResponseand, by extension, any custom middleware (any subclass ofBaseHTTPMiddleware) 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: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:
Suspected cause:
Starlette calls both the
receive()(to check for disconnects) andsend()(to send the response) concurrently in itsStreamingResponsetype (since #839). It tries to wait for either of those two options to complete (usingasyncio.wait({tasks}, return_when=asyncio.FIRST_COMPLETED)). Because the ASGI dispatch in httpx is generally effectively synchronous, bothsend()andreceive()may never return control to the event loop when called which means that if either task that is waited on does not complete, thenasyncio.wait()will not return and the program will remain stuck in an infinite loop inside theStreamingResponse.listen_for_disconnectmethod.