Swagger Was Live in Production and the Code Looked Fine
A missing key in config.ini. A default that wasn't wrong. And a method that didn't do what we thought it did.
We found it during a routine check before a client demo: the Swagger UI was live on the production URL. Fully interactive. Every endpoint listed, every schema visible, try-it-out enabled. For an enterprise health platform with a pending SOC audit, this was not a small problem.
We looked at the code. Everything looked correct.
The setup
The FastAPI app read its configuration from a config.ini file via Python's configparser. Environment name, debug mode, database URL — all in there. The Swagger disable logic was tied to the environment: if the environment was production, docs should be off.
# config.ini
[app]
debug = false
db_url = postgresql://...
# APP_ENV key not present in this file
# settings.py
config = configparser.ConfigParser()
config.read('config.ini')
APP_ENV = config.get('app', 'app_env', fallback='development')
# app.py
app = FastAPI()
if APP_ENV != 'production':
app.docs_url = '/docs'
app.redoc_url = '/redoc'
else:
app.docs_url = None
app.redoc_url = NoneRead that and you'll see the same thing we saw: the logic looks right. If app_env isn't "production", show docs. Otherwise, don't. Reasonable.
Bug one: the missing key
The app_env key was not in config.ini. The configparser fallback returned "development". In production. So APP_ENV was always "development", the condition was always true, and docs were always enabled — regardless of what environment the server was actually running in.
This was subtle because there was no error. Missing config keys with fallbacks are silent. The app started fine, ran fine, returned the right data — and quietly told anyone who asked that it was a development server with full API documentation.
Bug two: the wrong method
Once we spotted the missing key, we added it. Redeployed. Swagger was still there.
This is where it got frustrating. Now APP_ENV was correctly reading "production". The condition was correct. The assignment was correct. But the Swagger UI was still reachable.
FastAPI bakes the docs_url and redoc_url into the application at instantiation time. Setting `app.docs_url = None` after `FastAPI()` has already been called does not remove the routes. The routes were registered at startup and they stay.
The if-else block was modifying attributes on an already-initialized object. The logic ran, the attributes changed, and FastAPI ignored them — because it had already done the work those attributes were meant to configure.
The fix
The configuration has to happen before the FastAPI instance is created. The docs_url and redoc_url belong in the constructor, not in a post-init assignment:
# settings.py
APP_ENV = os.getenv('APP_ENV') or config.get('app', 'app_env', fallback='development')
# app.py
is_production = APP_ENV == 'production'
app = FastAPI(
title="Vaidya API",
docs_url=None if is_production else '/docs',
redoc_url=None if is_production else '/redoc',
openapi_url=None if is_production else '/openapi.json',
)We also added an environment variable path as the primary source (os.getenv), with config.ini as the fallback — so the production server can set APP_ENV at the OS level without relying on a file that could be misconfigured.
Why it was hard to find
Two bugs masked each other. The missing key meant the condition never evaluated to "production", so the post-init assignment never ran — which meant we never noticed the assignment didn't work. When we added the key, the assignment ran, we confirmed APP_ENV was correct, and still Swagger was there. That was the moment we realized the method itself was wrong.
In isolation, either bug would have been obvious. Together, they took longer because fixing one revealed the other.
- ›Config fallbacks are silent. A missing key that falls back to a default is not an error — which means you'll never know it's missing unless you check explicitly.
- ›FastAPI (and most frameworks) configure routes at startup. Post-init attribute changes don't retroactively modify what was registered.
- ›Prefer os.getenv over config file parsing for values that differ between environments. Environment variables are harder to accidentally omit.
- ›In production, openapi_url should also be None — otherwise the schema is accessible even if the UI isn't.
- ›If Swagger is supposed to be off in production, add a test that confirms it. A single HTTP GET to /docs in a prod smoke test would have caught this immediately.