Skip to content

Docs and OpenAPI endpoints

Severity: mediumApplies to: FastAPI 0.100+Facts last verified 2026-08-13 against FastAPI 0.141.1

Three endpoints exist on a default FastAPI() and all three return 200 with no configuration:

Constructor parameter Default
docs_url '/docs'
redoc_url '/redoc'
openapi_url '/openapi.json'
swagger_ui_oauth2_redirect_url '/docs/oauth2-redirect'

The interactive pages get the attention, but /openapi.json is the one that matters. Turning off /docs and /redoc while leaving the schema up removes the human interface and keeps the machine-readable one — which is the wrong half to keep.

Every path and method. Every path, query and header parameter, with names and types. Every request and response model, with all their field names. The security schemes you declared, which names your auth mechanism and where the token endpoint is. And your route descriptions and docstrings, which are written for colleagues and read accordingly.

That is a reconnaissance document produced automatically and kept current by construction. It does not disclose data and it is not a vulnerability — it removes the guesswork from finding the endpoints that might be.

Because the schema is generated from your models, it also publishes field names you may not have intended to expose. That overlaps with response_model, where the same schema is the verification tool.

settings on this page
openapi_urloption

The machine-readable schema, and the one that matters most — it is the complete map of every route, parameter and model. Setting it to None also takes down /docs and /redoc, because both are rendered from it; disabling only the two UIs leaves this served.

accepts
"/openapi.json" | None (disables the schema and both UIs)
default
"/openapi.json"
set in
FastAPI(...)
Read in: fastapi 0.141.1 fastapi/applications.py — FastAPI.__init__ signature
docs_urloption

Swagger UI. On by default, at a path anyone would guess first.

accepts
"/docs" | None (disables)
default
"/docs"
set in
FastAPI(...)
Read in: fastapi 0.141.1 fastapi/applications.py — FastAPI.__init__ signature
redoc_urloption

The second documentation UI, and the one people forget — disabling docs_url alone leaves this one serving the same information.

accepts
"/redoc" | None (disables)
default
"/redoc"
set in
FastAPI(...)
Read in: fastapi 0.141.1 fastapi/applications.py — FastAPI.__init__ signature
swagger_ui_oauth2_redirect_urloption

The OAuth2 callback Swagger UI uses. A third default route under /docs, worth knowing when you are enumerating what the app exposes rather than assuming two URLs.

accepts
"/docs/oauth2-redirect" | None
default
"/docs/oauth2-redirect"
set in
FastAPI(...)
Read in: fastapi 0.141.1 fastapi/applications.py — FastAPI.__init__ signature
the fix
import os
from fastapi import FastAPI
IS_PROD = os.getenv("ENV") == "production"
app = FastAPI(
docs_url=None if IS_PROD else "/docs",
redoc_url=None if IS_PROD else "/redoc",
openapi_url=None if IS_PROD else "/openapi.json",
)

None removes the route rather than protecting it. Setting openapi_url=None also disables /docs and /redoc, since both fetch the schema — so if you only change one line, make it that one.

verify it workedRun this in: http response
Terminal window
for p in /docs /redoc /openapi.json /docs/oauth2-redirect; do
printf '%-24s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://api.example.com$p)"
done

404 on all four is the goal. A 200 on /openapi.json with 404 on /docs is the half-configured case, and it is the common one.

Internal APIs often want the docs. Serving them behind a dependency is reasonable — disable the built-ins and re-add them guarded:

from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
@app.get("/internal/openapi.json", include_in_schema=False)
def schema(user=Depends(require_staff)):
return get_openapi(title=app.title, version=app.version, routes=app.routes)
@app.get("/internal/docs", include_in_schema=False)
def docs(user=Depends(require_staff)):
return get_swagger_ui_html(openapi_url="/internal/openapi.json", title="docs")

include_in_schema=False keeps these two out of the document they serve. Note this is worth doing for its own sake: an endpoint marked that way is absent from the schema but still routable, so it is a documentation choice and not an access control. Anything genuinely sensitive needs the dependency, which is why require_staff is on both routes above and not just the UI one.

Changing docs_url to /internal-docs-8f3a is a common half-measure. It raises the cost of finding the path and nothing else — the URL appears in browser history, in proxy access logs, in referrer headers, and in any screenshot anyone pastes into chat.

Obscurity here is a speed bump, and it is worth being honest that this control is mostly about reducing the free reconnaissance rather than closing an exposure. That is why it is rated medium: on its own, a published schema harms nothing.

before you ship this

Disabling the schema breaks anything that consumes it, and those consumers are easy to forget because they are not your front end: generated client SDKs, contract tests, API gateway definitions imported from the spec, Postman collections synced from the URL, and internal developer portals.

A generation step that suddenly fetches a 404 usually fails loudly, which is the good case. A contract test that silently skips when it cannot reach the schema is the bad one — check that yours fails rather than passes when the schema is gone.

The workable pattern is to generate the spec at build time and publish it as an artefact, so the file continues to exist for tooling while the running application stops serving it:

Terminal window
python3 -c 'import json; from app.main import app; print(json.dumps(app.openapi()))' > openapi.json

That runs in CI, needs no live endpoint, and gives you a versioned spec per release.