Skip to content

SchemaExtension.on_operation can exit before sibling async field work is finished #4326

@MeRuslan

Description

@MeRuslan

Describe the Bug

If two sibling async fields run together and one raises, on_operation can exit before the other field has finished.

This makes on_operation unsafe as a strict "request is fully done" boundary for closing shared async resources.

System Information

  • Operating system: macOS 15.4
  • Python version: 3.14.0
  • Strawberry version (if applicable): 0.312.0

Additional Context

Minimal demonstration is below.

Observed order:

operation enter
slow start
boom raise
operation exit
execute returned
slow finish
So on_operation exited before all field work was done.

Code:

import asyncio
import logging

import strawberry
from strawberry.extensions import SchemaExtension


timeline: list[str] = []
slow_started = asyncio.Event()


class ProbeExtension(SchemaExtension):
    async def on_operation(self):
        timeline.append("operation enter")
        try:
            yield
        finally:
            timeline.append("operation exit")


@strawberry.type
class Query:
    @strawberry.field
    async def slow(self) -> str:
        timeline.append("slow start")
        slow_started.set()
        await asyncio.sleep(0.05)
        timeline.append("slow finish")
        return "slow"

    @strawberry.field
    async def boom(self) -> str:
        await slow_started.wait()
        timeline.append("boom raise")
        raise RuntimeError("boom")


schema = strawberry.Schema(query=Query, extensions=[ProbeExtension])


async def run_demo() -> None:
    logging.getLogger("strawberry.execution").setLevel(logging.CRITICAL)

    result = await schema.execute("{ slow boom }")
    timeline.append("execute returned")

    # Let the still-running sibling resolver finish so the ordering is visible.
    await asyncio.sleep(0.1)
    timeline.append("after extra sleep")

    print("GraphQL errors:", [error.message for error in result.errors or []])
    print()
    print("Timeline:")
    for entry in timeline:
        print(entry)

    operation_exit_idx = timeline.index("operation exit")
    slow_finish_idx = timeline.index("slow finish")
    assert operation_exit_idx < slow_finish_idx, timeline

    print()
    print("Observed: `on_operation` exited before `slow` finished.")


def main() -> None:
    asyncio.run(run_demo())


if __name__ == "__main__":
    main()

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions