Skip to content

Raising a Problem through a yield dependency fails with FrozenInstanceError #9

Description

@PythonFZ

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions