@@ -693,9 +693,32 @@ def _create_streaming_parameter_wrapper(
693693 parsing."""
694694 is_async_gen = inspect .isasyncgenfunction (handler )
695695
696+ # NOTE:
697+ # -----
698+ # FastAPI >= 0.123.5 uses Dependant.is_coroutine_callable, which in
699+ # turn unwraps callables via inspect.unwrap() and then inspects the
700+ # unwrapped target to decide whether it is a coroutine function /
701+ # generator / async generator.
702+ #
703+ # If we decorate an async-generator handler with
704+ # functools.wraps(handler), FastAPI will unwrap back to the original
705+ # async-generator function and # *misclassify* the endpoint as
706+ # non-coroutine. It will then call our async wrapper *without awaiting
707+ # it*, and later try to JSON-encode the resulting coroutine object,
708+ # causing errors like:
709+ # TypeError("'coroutine' object is not iterable")
710+ #
711+ # To avoid that, we deliberately do NOT use functools.wraps() here.
712+ # Instead, we manually copy the key metadata (name, qualname, doc,
713+ # module, and signature) from the original handler, but we do NOT set
714+ # __wrapped__. This ensures:
715+ # * FastAPI sees the wrapper itself as the callable (an async def),
716+ # so Dependant.is_coroutine_callable is True, and it is properly
717+ # awaited.
718+ # * FastAPI still sees the correct signature for parameter parsing.
719+
696720 if is_async_gen :
697721
698- @functools .wraps (handler )
699722 async def wrapped_handler (* args , ** kwargs ):
700723 async def generate ():
701724 try :
@@ -720,12 +743,8 @@ async def generate():
720743 media_type = "text/event-stream" ,
721744 )
722745
723- wrapped_handler .__signature__ = inspect .signature (handler )
724- return wrapped_handler
725-
726746 else :
727747
728- @functools .wraps (handler )
729748 def wrapped_handler (* args , ** kwargs ):
730749 def generate ():
731750 try :
@@ -748,8 +767,39 @@ def generate():
748767 media_type = "text/event-stream" ,
749768 )
750769
751- wrapped_handler .__signature__ = inspect .signature (handler )
752- return wrapped_handler
770+ # Manually propagate essential metadata without creating a __wrapped__
771+ # chain that would confuse FastAPI's unwrap logic.
772+ wrapped_handler .__name__ = getattr (
773+ handler ,
774+ "__name__" ,
775+ wrapped_handler .__name__ ,
776+ )
777+ wrapped_handler .__qualname__ = getattr (
778+ handler ,
779+ "__qualname__" ,
780+ wrapped_handler .__qualname__ ,
781+ )
782+ wrapped_handler .__doc__ = getattr (
783+ handler ,
784+ "__doc__" ,
785+ wrapped_handler .__doc__ ,
786+ )
787+ wrapped_handler .__module__ = getattr (
788+ handler ,
789+ "__module__" ,
790+ wrapped_handler .__module__ ,
791+ )
792+ wrapped_handler .__signature__ = inspect .signature (handler )
793+
794+ # Make sure FastAPI doesn't see any stale __wrapped__ pointing back to
795+ # the original async-generator; if present, remove it.
796+ if hasattr (wrapped_handler , "__wrapped__" ):
797+ try :
798+ delattr (wrapped_handler , "__wrapped__" )
799+ except Exception : # pragma: no cover - very defensive
800+ pass
801+
802+ return wrapped_handler
753803
754804 @staticmethod
755805 def _add_custom_endpoints (app : FastAPI ):
0 commit comments