-
-
Notifications
You must be signed in to change notification settings - Fork 130
Open
Description
Hi!
Description
When using the following combination...
- "Embedded Granian" where an asyncio task is used to run the
server.serve()coroutine. - A handler returns a
starlette.responses.StreamingResponse. - The response's body is generated by an ordinary, non-async generator.
... the application panics on exit.
Steps to Reproduce
Here is an MRE that triggers the problem:
repro.py:
# /// script
# dependencies = [
# "granian==2.5.4",
# "starlette==0.48.0",
# ]
# ///
import asyncio
import contextlib
import itertools
import string
import sys
from typing import AsyncGenerator, Generator
import granian.server.embed
import granian.constants
from starlette.routing import Route
from starlette.responses import StreamingResponse
from starlette.requests import Request
from starlette.applications import Starlette
def sync_generator() -> Generator[str]:
for batch in itertools.batched(string.ascii_letters, n=4):
yield "".join(batch)
async def async_generator() -> AsyncGenerator[str]:
for batch in itertools.batched(string.ascii_letters, n=4):
yield "".join(batch)
await asyncio.sleep(0.1)
async def sync_endpoint(_: Request) -> StreamingResponse:
"""Endpoint where the content is created by an ordinary generator."""
generator = sync_generator()
return StreamingResponse(generator, media_type="text/plain")
async def async_endpoint(_: Request) -> StreamingResponse:
"""Endpoint where the content comes from an asynchronous generator."""
generator = async_generator()
return StreamingResponse(generator, media_type="text/plain")
@contextlib.asynccontextmanager
async def run_web_application() -> AsyncGenerator[None]:
web_application = Starlette(
routes=[
Route("/sync_endpoint", sync_endpoint),
Route("/async_endpoint", async_endpoint),
]
)
server = granian.server.embed.Server(
web_application,
interface=granian.constants.Interfaces.ASGI,
log_access=True,
)
server_task = asyncio.create_task(server.serve())
try:
yield
finally:
server.stop() # type: ignore[no-untyped-call]
await server_task
async def amain(endpoint: str) -> None:
async with run_web_application():
await asyncio.sleep(1)
# Simulate someone accessing the endpoint.
proc = await asyncio.create_subprocess_exec(
"curl", "-w", "\n", f"http://127.0.0.1:8000/{endpoint}"
)
await proc.wait()
await asyncio.sleep(1)
if __name__ == "__main__":
print(f"{sys.platform = }")
print(f"{sys.version = }")
asyncio.run(amain(sys.argv[1]))Running the script so that it accesses its endpoint with the async generator works without problems:
$ RUST_BACKTRACE=full uv run repro.py async_endpoint
sys.platform = 'linux'
sys.version = '3.13.7 (main, Nov 10 2011, 15:00:00) [GCC 15.2.0]'
[WARNING] Embedded server is experimental!
[INFO] Starting granian (embedded)
[INFO] Listening at: http://127.0.0.1:8000
[INFO] Spawning worker-1 with id: 140519357957584
[INFO] Started worker-1
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
[2025-09-21 12:03:08 +0200] 127.0.0.1 - "GET /async_endpoint HTTP/1.1" 200 1329.077
[INFO] Shutting down granian
[INFO] Stopping worker-1
[INFO] Stopping worker-1
When the endpoint with the sync generator is accessed, the panic happens on exit:
$ RUST_BACKTRACE=full uv run repro.py sync_endpoint
sys.platform = 'linux'
sys.version = '3.13.7 (main, Nov 10 2011, 15:00:00) [GCC 15.2.0]'
[WARNING] Embedded server is experimental!
[INFO] Starting granian (embedded)
[INFO] Listening at: http://127.0.0.1:8000
[INFO] Spawning worker-1 with id: 139769539970512
[INFO] Started worker-1
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
[2025-09-21 12:05:58 +0200] 127.0.0.1 - "GET /sync_endpoint HTTP/1.1" 200 31.009
[INFO] Shutting down granian
[INFO] Stopping worker-1
[INFO] Stopping worker-1
thread '<unnamed>' panicked at library/core/src/panicking.rs:225:5:
panic in a function that cannot unwind
stack backtrace:
0: 0x7f1ea2d62f22 - <unknown>
1: 0x7f1ea2c9d6c3 - <unknown>
2: 0x7f1ea2d62a7f - <unknown>
3: 0x7f1ea2d62d63 - <unknown>
4: 0x7f1ea2d626ed - <unknown>
5: 0x7f1ea2d8e4f5 - <unknown>
6: 0x7f1ea2d8e489 - <unknown>
7: 0x7f1ea2d8f19c - <unknown>
8: 0x7f1ea265d54c - <unknown>
9: 0x7f1ea265d5b5 - <unknown>
10: 0x7f1ea265d563 - <unknown>
11: 0x7f1ea2d8f6b0 - <unknown>
12: 0x7f1ea449a56a - <unknown>
13: 0x7f1ea451de54 - clone
14: 0x0 - <unknown>
thread caused non-unwinding panic. aborting.
Metadata
Metadata
Assignees
Labels
No labels