obsidian/wiki/concepts/fastapi-root-path-route-stripping.md
2026-05-06 21:05:03 +01:00

3.7 KiB

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
daily/2026-05-06.md
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).

Sources

  • daily/2026-05-06.md — BAIC dashboard deployment: routes registering with full prefix caused 404s