Skip to content

Crash on exit when using embedded Granian and Starlette StreamingResponse #693

@JenSte

Description

@JenSte

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions