-
Notifications
You must be signed in to change notification settings - Fork 328
Description
Describe the bug
When an on_startup hook raises a chained exception (raise B from A), faststream silently discards the underlying cause.
Only the outermost exception appears in the error output — the underlying cause that triggered the failure is permanently lost before uvicorn (or any other ASGI server) ever sees it.
The mechanism is in start_lifespan_context (faststream/asgi/app.py): anyio's task group wraps startup exceptions into an ExceptionGroup, and faststream unwraps them with:
except ExceptionGroup as e:
for ex in e.exceptions:
raise ex from None # ← clears ex.__cause__ and sets __suppress_context__raise ex from None has two effects: it suppresses the ExceptionGroup context (desirable), but it also unconditionally clears ex.__cause__ (destructive).
Any chain the user deliberately set with raise B from A is erased in place before the exception reaches the ASGI server's logger.
How to reproduce
# repro.py — run with: uvicorn repro:app
# (NATS does not need to be reachable)
from faststream.asgi.app import AsgiFastStream
from faststream.nats import NatsBroker
broker = NatsBroker("nats://localhost:4222")
app = AsgiFastStream(broker)
@app.on_startup
async def startup() -> None:
try:
raise ConnectionError("underlying cause: could not reach service on port 3306")
except ConnectionError as e:
raise RuntimeError("application startup failed") from eExpected behavior
Python's standard exception formatting preserves chained exceptions and prints both, separated by "The above exception was the direct cause of the following exception:":
ConnectionError: underlying cause: could not reach service on port 3306
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
RuntimeError: application startup failedObserved behavior
Only the outermost exception is logged.
The ConnectionError underlying cause is completely absent:
ERROR: Traceback (most recent call last):
...
RuntimeError: application startup failed
ERROR: Application startup failed. Exiting.RuntimeError.__cause__ is None by the time uvicorn receives the exception, because raise ex from None cleared it inside faststream.
Screenshots
N/A
Environment
Running FastStream 0.6.6 with CPython 3.14.2 on LinuxAdditional context
The minimal fix would be to preserve a user-set __cause__ when re-raising from the ExceptionGroup:
except ExceptionGroup as e:
for ex in e.exceptions:
raise ex from ex.__cause__ # preserve intentional chain; None if unsetThis still suppresses the noisy ExceptionGroup context (the displayed traceback will not mention ExceptionGroup), but it no longer discards the __cause__ the user explicitly set.