An external caller reports a bug and pastes your API's error response into the
ticket — and it contains your insides: a full Python traceback with absolute file
paths (/home/ubuntu/churn-service/src/pipeline.py, line 43), library names and
versions, the internal function names, the model file's location, and a fragment
of the offending data row. Sometimes the security team finds it first, in a
partner's screenshot or a public forum post asking "what does this error mean?".
A FastAPI (or Flask/Django) service that reached production with development
settings: debug or reload mode on, no exception handler registered, or a
hand-written handler that helpfully returns str(exc) or the traceback "to make
debugging easier". Inside the team that felt harmless — everyone had repo access
anyway. Then the endpoint was opened to a partner, a mobile app, or the internet,
and every unhandled bug became a broadcast. Under India's DPDP expectations and
any bank's security review, echoing internals (and worse, echoing the customer
data that triggered the crash) to arbitrary callers is a reportable finding.
Linked modules: 4.2 (Serving with FastAPI), 3.11 (Safety and DPDP basics).
A stack trace is written for the team that owns the code; sending it outward hands a stranger a map — directory layout, framework and library versions to match against known exploits, internal naming, and sometimes live data values. The root cause is a missing policy, not a missing feature: nobody decided what a caller may see when the service fails, so the framework's default (or a debug convenience) decided it. Confirm the exposure by forcing a crash on purpose against the running service — send a payload that triggers an unhandled path — and reading the response exactly as an outsider would. If the response names any file path, version or variable, the leak is real, whatever the settings claim.
Separate the two audiences. Register a global exception handler that returns one
generic body — {"detail": "internal error — the team has the log"} with status
500 and, ideally, a request id — and logs the full traceback with
log.exception(...) for the team. Turn off debug and reload flags in production;
they exist for the laptop. Re-run the forced-crash test and confirm the caller
now sees only the generic line while the complete story, with the request id,
sits in the server log. If real tracebacks already leaked, treat it as an
incident: review what was exposed, rotate anything sensitive that appeared, and
tell the security owner.
Make the 500 path a tested feature, not an accident: keep one deliberate
crash-and-check in your test file so a regression to leaky errors fails loudly.
Policy by status code — 422 says what field broke (pydantic writes it), 400 says
which rule broke (one short sentence), 500 says nothing but "we know" — and logs
carry everything, keyed by request id. Never put str(exc) in a response body,
and never let debug mode near a deployment; module 4.6's CI will encode both as
checks.
Q: "What should an external caller see when your API hits an unhandled bug, and what should your team see?" Strong answer: two different artifacts — the caller gets a generic 500 with a request id, the team gets the full traceback in logs, linked by that id. Explain WHY tracebacks are dangerous outward: paths, versions and data values are reconnaissance material, and echoing customer data may itself be a privacy breach. Bonus signal: describing the forced-crash test — deliberately breaking your own service and reading the response as a stranger — shows you verify the failure path, not only the happy path.