Debug mode and tracebacks
FastAPI(debug=...) defaults to False, and that default is correct. This page is
therefore a verify page: the question is not whether to turn it off but whether
something in your deployment turns it on.
What changes
Section titled “What changes”The same unhandled RuntimeError, raised from the same endpoint:
debug |
Status | Response body |
|---|---|---|
False |
500 | Internal Server Error — 21 bytes |
True |
500 | Full traceback as HTML — 5,169 bytes |
The debug body is Starlette’s ServerErrorMiddleware rendering the exception: the
traceback, the source file paths, the surrounding source lines of each frame, and the
exception message. It is a development tool and it is genuinely useful — it is just
addressed to whoever made the request.
What it discloses
Section titled “What it discloses”Absolute filesystem paths, which name your deployment layout and often the OS user. Installed package versions, from the frames that pass through site-packages. Your module and function names, which map the application. And the surrounding source lines of each frame, which is the part people underestimate — a frame inside a function that builds a query publishes that query’s construction.
It does not print local variable values, so it discloses less than some frameworks’ debug pages. That is why this is rated high rather than critical: it is a strong map of the application rather than a direct credential leak.
debugoptionReturns the full traceback page on an unhandled error instead of a bare 500. The default is correct, so this is a page about something you switched on rather than something you forgot — and the usual route in is an environment variable feeding the constructor, not a literal True in the source.
- accepts
True|False- default
False- set in
FastAPI(...)
import osfrom fastapi import FastAPI
app = FastAPI( debug=False, # explicit; never read this from an env var docs_url=None if os.getenv("ENV") == "production" else "/docs",)The value being a literal is the point. debug=os.getenv("DEBUG") is the failure mode
this control exists for: any non-empty string, including "0" and "false", is
truthy in Python.
debug=os.getenv("DEBUG") # "false" -> True. wrong.debug=os.getenv("DEBUG") == "1" # explicit comparison. correct.# force an unhandled error on a route that does not exist in a safe form,# or hit a known-bad path, and read the body size and shapecurl -s -o /tmp/err.html -w '%{http_code} %{size_download}\n' \ https://api.example.com/__does_not_exist__/
grep -ciE 'Traceback|site-packages|File "/' /tmp/err.htmlA production application returns a short body and zero matches. Any hit for
Traceback or site-packages means the debug renderer is live.
Check the setting directly if you can reach the process:
python3 -c "from app.main import app; print('debug =', app.debug)"--reload is the one that catches people
Section titled “--reload is the one that catches people”uvicorn --reload is a development flag, and it tends to travel in the same
command: line as everything else in a container image. It does not itself set
app.debug, so the two are worth checking separately — but a deployment running with
--reload is a deployment running the development invocation, and the debug flag is
usually nearby.
# in the running containerps -o args= -C uvicorn 2>/dev/null || ps aux | grep '[u]vicorn'--reload in that output means the image is running its development command.
An exception handler does not close this
Section titled “An exception handler does not close this”A custom handler for Exception intercepts errors your application raises, which is
worth having. It does not change what ServerErrorMiddleware does with an exception
raised before your handler is reached — in middleware, in dependency resolution, or
during request parsing.
So a handler is a good addition and a bad substitute. debug=False is what actually
governs the rendered traceback.
Turning debug off removes the diagnostic you were relying on, and the replacement
has to exist before you take it away — otherwise the next 500 is a blank
Internal Server Error with nothing behind it.
Make sure the traceback goes somewhere first:
import logginglogger = logging.getLogger("app")
@app.exception_handler(Exception)async def unhandled(request, exc): logger.exception("unhandled error on %s %s", request.method, request.url.path) return JSONResponse({"detail": "Internal Server Error"}, status_code=500)logger.exception records the traceback server-side. The client gets nothing useful,
which is the entire point.
Confirm those records are actually reaching your log destination before you deploy the change — a handler that logs to a stream nobody collects is the same blackout with extra steps.
Related
Section titled “Related”- Validation error disclosure — what the 422 body gives away even with debug off
- Docs endpoints — the other development affordance that ships by default