granian icon indicating copy to clipboard operation
granian copied to clipboard

AttributeError: 'granian._granian.PyFutureAwaitable' object has no attribute 'throw'

Open hermeschen1116 opened this issue 2 months ago • 4 comments

I got this exception in my streaming response API. It seems there's a method is not implemented for granian to handle FastAPI Streaming Response. And BTW, I run my service on unix socket in this cast.

2025-09-26T01:33:46.840801Z [error    ] Application callable raised an exception
Traceback (most recent call last):
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/granian/_futures.py", line 15, in future_watcher
    await inner(watcher.scope, watcher.proto)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/fastapi/applications.py", line 1082, in __call__
    await super().__call__(scope, receive, send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/applications.py", line 113, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 186, in __call__
    raise exc
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 63, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/routing.py", line 716, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/routing.py", line 736, in app
    await route.handle(scope, receive, send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/routing.py", line 290, in handle
    await self.app(scope, receive, send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/routing.py", line 78, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/routing.py", line 76, in app
    await response(scope, receive, send)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/responses.py", line 270, in __call__
    with collapse_excgroups():
         ^^^^^^^^^^^^^^^^^^^^
  File "/Users/hermeschen/.local/share/uv/python/cpython-3.12-macos-aarch64-none/lib/python3.12/contextlib.py", line 158, in __exit__
    self.gen.throw(value)
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/_utils.py", line 85, in collapse_excgroups
    raise exc
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/responses.py", line 274, in wrap
    await func()
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/starlette/responses.py", line 254, in stream_response
    async for chunk in self.body_iterator:
  File "/Users/hermeschen/Repo/work/taiwan-license-plate-recognition/.venv/lib/python3.12/site-packages/stream_server/routers/Main.py", line 134, in frame_streamer
AttributeError: 'granian._granian.PyFutureAwaitable' object has no attribute 'throw'

hermeschen1116 avatar Sep 26 '25 01:09 hermeschen1116

It seems like something in your code is treating Granian futures as coroutines. Can you provide a MRE?

gi0baro avatar Sep 26 '25 08:09 gi0baro

Here's my API. The error occurred when I tried to use httpx to request this API.

@router.get(
	"/capture/stream",
	status_code=status.HTTP_200_OK,
	responses={
		"200": {"description": "回傳串流擷取結果"},
		"404": {"description": "不存在的串流"},
		"422": {"description": "錯誤的 UUID 格式"},
		"503": {"description": "擷取串流時出現錯誤"},
	},
	description="擷取指定串流畫面",
)
async def stream_frame(
	request: Request,
	stream_id: typing.Annotated[
		uuid.UUID, Query(description="串流來源 ID", example="07c9f116-feb1-5e24-97fc-f400c03e935a")
	],
):
	"""擷取串流畫面"""

	if not request.app.state.streams:
		await LOGGER.awarning(msg := "🏞️ 無任何串流")
		raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=msg)

	if request.app.state.streams.get(stream_id) is None:
		await LOGGER.awarning(msg := f"🏞️ 串流(stream_id:{stream_id})不存在")
		raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=msg)

	async def frame_streamer():
		frame_queue: CyclicPriorityQueue = await request.app.state.frame_buffer.register()
		try:
			while not (await request.is_disconnected()):
				_, frames = await frame_queue.get()
				if (image := frames[stream_id].frame) is None:
					continue
				yield (
					b"--frame\r\nContent-Type: image/webp\r\n\r\n"
					+ image
					+ b"\r\nX-TIMESTAMP: "
					+ frames[stream_id].timestamp.isoformat().encode("utf-8")
					+ b"\r\nX-STREAM-ID: "
					+ str(frames[stream_id].stream_id).encode("utf-8")
					+ b"\r\n"
				)
				await LOGGER.ainfo(f"🏞️ 擷取串流畫面({frames[stream_id].timestamp})")
		except asyncio.CancelledError as e:
			await LOGGER.awarning(f"⚠️ Client({request.client})中斷連接")
			raise e
		finally:
			await request.app.state.frame_buffer.unregister(frame_queue)

	return StreamingResponse(frame_streamer(), media_type="multipart/x-mixed-replace; boundary=frame")

hermeschen1116 avatar Sep 26 '25 18:09 hermeschen1116

I think the problematic line is while not (await request.is_disconnected()):

Not a starlette/FastAPI expert, but it seems (for reasons) on exceptions the receive future from Granian gets treated as a coroutine (but it's a future, so shouldn't be iterated with send() and throw() calls, it should be cancelled with cancel() instead). Maybe @Kludex or @tiangolo can help further here.

gi0baro avatar Sep 29 '25 15:09 gi0baro

Okay, thank you.

hermeschen1116 avatar Oct 06 '25 05:10 hermeschen1116