| title |
aliases |
tags |
sources |
created |
updated |
| FastAPI root_path Does Not Add a Prefix — Starlette Strips It Before Route Matching |
| fastapi-root-path |
| starlette-root-path-routes |
| fastapi-behind-prefix |
|
| fastapi |
| starlette |
| routing |
| deployment |
| gotcha |
| apache |
| reverse-proxy |
|
|
2026-05-06 |
2026-05-06 |
FastAPI root_path Does Not Add a Prefix — Starlette Strips It Before Route Matching
When FastAPI is initialised with root_path="/cc-dashboard", Starlette strips that prefix from the incoming URL before matching routes. Routes must therefore be registered without the prefix. Registering /cc-dashboard/healthz results in a permanent 404 — the router never sees the prefix in the path.
Key Takeaways
root_path is metadata for OpenAPI docs and proxy headers only — it is NOT prepended to route paths
- Starlette removes
root_path from the URL before route matching; routes see the stripped path
- Register
/healthz, not /cc-dashboard/healthz, even when the app is served at /cc-dashboard/
- The Apache
ProxyPass /cc-dashboard/ http://127.0.0.1:8001/ strips the prefix before forwarding — FastAPI's root_path merely tells the app "I am mounted at this prefix" for docs generation
- This behaviour is not documented on the FastAPI site; it is a Starlette ASGI mount convention
Details
The Counterintuitive Initialisation
# app is served by Apache at /cc-dashboard/
app = FastAPI(root_path="/cc-dashboard")
# ✅ CORRECT — Starlette strips root_path, route matches /healthz
@app.get("/healthz")
async def healthz():
return {"ok": True}
# ❌ WRONG — route registered as /cc-dashboard/healthz
# but Starlette sees /healthz after stripping, so this never matches
@app.get("/cc-dashboard/healthz")
async def healthz_wrong():
return {"ok": True}
How the Request Flows
Browser → GET /cc-dashboard/healthz
↓
Apache ProxyPass /cc-dashboard/ http://127.0.0.1:8001/
↓ (Apache strips /cc-dashboard prefix)
Starlette receives → GET /healthz (root_path="/cc-dashboard" stored separately)
↓
Route matching: /healthz ← this is what must be registered
What root_path Actually Does
# root_path is used by Starlette for:
# 1. OpenAPI/Swagger docs — generates correct absolute URLs in the schema
# 2. url_for() — builds URLs with the prefix included
# 3. Request.root_path — accessible in middleware for logging/tracing
app = FastAPI(root_path="/cc-dashboard")
# Swagger UI served at /cc-dashboard/docs will use /cc-dashboard/openapi.json
# url_for("healthz") → "/cc-dashboard/healthz"
Corresponding Apache Config
# Apache strips /cc-dashboard before proxying
ProxyPass /cc-dashboard/ http://127.0.0.1:8001/
ProxyPassReverse /cc-dashboard/ http://127.0.0.1:8001/
# Trailing slash on BOTH sides is mandatory —
# without it Apache passes the prefix intact → all routes 404
[!warning] ProxyPassMatch stripping variant
If using ProxyPassMatch ^/cc-dashboard/(.*)$ http://127.0.0.1:8001/$1, the $1 capture group performs the same stripping manually. This is needed when you cannot use the simple ProxyPass /prefix/ http://host/ form (e.g. when an Alias conflict exists — see wiki/concepts/apache-mod-alias-proxy-priority).
Related
Sources
- daily/2026-05-06.md — BAIC dashboard deployment: routes registering with full prefix caused 404s