Summary
Raising a Problem from inside (or through) a FastAPI yield dependency fails with
dataclasses.FrozenInstanceError: cannot assign to field '__traceback__', so the intended
problem response is masked as a generic 500.
Cause
_ProblemMeta makes every Problem a frozen=True Pydantic dataclass. FastAPI runs yield
dependencies inside an AsyncExitStack; when an exception propagates back out, contextlib's
(async) generator context manager reassigns exc.__traceback__ (and may touch __cause__ /
__context__). A frozen dataclass forbids that assignment, turning any raised Problem into a
FrozenInstanceError.
The exception's initial raise works because CPython sets __traceback__ at the C level; only the
explicit Python-level reassignment during unwinding through a context manager trips the freeze.
The path is never exercised by routes without yield dependencies, which is why it passes
unnoticed there.
Reproduction
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from fastapi_rfc9457 import NotFound, add_problem_handlers
app = FastAPI()
add_problem_handlers(app)
async def yield_dep():
yield 1
class Missing(NotFound):
type = "/problems/missing"
@app.get("/no-dep")
async def no_dep():
raise Missing(detail="nope")
@app.get("/via-yield")
async def via_yield(_=Depends(yield_dep)):
raise Missing(detail="nope")
client = TestClient(app, raise_server_exceptions=False)
print(client.get("/no-dep").status_code) # 404 (correct)
print(client.get("/via-yield").status_code) # 500 (FrozenInstanceError)
With raise_server_exceptions=True, /via-yield raises:
dataclasses.FrozenInstanceError: cannot assign to field '__traceback__'
Suggested fix
Keep fields immutable but allow the BaseException dunders to be assigned, e.g. after building the
frozen dataclass in _ProblemMeta.__new__, wrap __setattr__ so that
__traceback__ / __cause__ / __context__ / __suppress_context__ / __notes__ go through
object.__setattr__ and everything else keeps the frozen behavior (or simply don't freeze).
A regression test should raise a Problem through a yield dependency and assert the documented
status code is returned (not 500).
Summary
Raising a
Problemfrom inside (or through) a FastAPIyielddependency fails withdataclasses.FrozenInstanceError: cannot assign to field '__traceback__', so the intendedproblem response is masked as a generic
500.Cause
_ProblemMetamakes everyProblemafrozen=TruePydantic dataclass. FastAPI runsyielddependencies inside an
AsyncExitStack; when an exception propagates back out,contextlib's(async) generator context manager reassigns
exc.__traceback__(and may touch__cause__/__context__). A frozen dataclass forbids that assignment, turning any raisedProbleminto aFrozenInstanceError.The exception's initial raise works because CPython sets
__traceback__at the C level; only theexplicit Python-level reassignment during unwinding through a context manager trips the freeze.
The path is never exercised by routes without
yielddependencies, which is why it passesunnoticed there.
Reproduction
With
raise_server_exceptions=True,/via-yieldraises:Suggested fix
Keep fields immutable but allow the
BaseExceptiondunders to be assigned, e.g. after building thefrozen dataclass in
_ProblemMeta.__new__, wrap__setattr__so that__traceback__/__cause__/__context__/__suppress_context__/__notes__go throughobject.__setattr__and everything else keeps the frozen behavior (or simply don't freeze).A regression test should raise a
Problemthrough ayielddependency and assert the documentedstatus code is returned (not
500).