Proxy and Root Path Configuration
When you deploy your app behind nginx with a path prefix like /api/v1, Swagger UI breaks because it requests /openapi.json instead of /api/v1/openapi.json. The root_path parameter in FastAPI fixes this by ensuring that all internal documentation links and the OpenAPI schema reflect the actual URL seen by external clients.
The Proxy Path Problem
In many production environments, a proxy (like Nginx, Traefik, or HAProxy) handles SSL termination and routing. A common pattern is to serve a FastAPI application under a specific path prefix:
- External URL:
https://example.com/api/v1/items - Proxy Configuration: Strips
/api/v1and forwards the request to the app. - Internal App URL:
http://localhost:8000/items
Because the FastAPI application only sees /items, it generates documentation links (like the URL for the OpenAPI schema) relative to the root /. When the browser tries to load these links from the external domain, they fail because the /api/v1 prefix is missing.
Configuring root_path
FastAPI provides the root_path parameter to bridge this gap. This parameter tells the application that it is being served behind a proxy that uses a specific path prefix.
You can set this directly in the FastAPI constructor in fastapi/applications.py:
from fastapi import FastAPI
app = FastAPI(root_path="/api/v1")
@app.get("/items")
def read_items():
return [{"item_id": "Foo"}]
Alternatively, you can provide this configuration at runtime using server flags if you are using Uvicorn:
uvicorn main:app --root-path /api/v1
When set via the command line, the ASGI server passes the value through the ASGI scope. FastAPI is designed to prioritize the root_path provided in the constructor as a default, but it dynamically respects the root_path found in the request scope during execution.
Internal Mechanics
The implementation of root_path in FastAPI involves two main stages: scope manipulation and dynamic route setup.
ASGI Scope Manipulation
In fastapi/applications.py, the FastAPI.__call__ method ensures that the configured root_path is present in the ASGI scope for every request. This allows any middleware or dependency to access the correct prefix.
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if self.root_path:
scope["root_path"] = self.root_path
await super().__call__(scope, receive, send)
Dynamic Documentation Routes
When FastAPI initializes documentation routes in the setup() method, it does not hardcode the URLs. Instead, it defines asynchronous handlers that inspect the root_path of the incoming request.
For example, the Swagger UI handler in fastapi/applications.py calculates the openapi_url by prepending the current root_path:
async def swagger_ui_html(req: Request) -> HTMLResponse:
root_path = req.scope.get("root_path", "").rstrip("/")
openapi_url = root_path + self.openapi_url
# ... returns the HTML with the corrected openapi_url
This ensures that if you access the documentation through the proxy, the browser correctly requests the schema from /api/v1/openapi.json.
OpenAPI Server Injection
To ensure that the "Try it out" button in Swagger UI works correctly, the generated OpenAPI schema must include the proxy's path in its servers list.
FastAPI handles this automatically inside the openapi handler defined in setup(). If a root_path is detected in the request scope, FastAPI injects it as the primary server URL in the schema:
async def openapi(req: Request) -> JSONResponse:
root_path = req.scope.get("root_path", "").rstrip("/")
schema = self.openapi()
if root_path and self.root_path_in_servers:
server_urls = {s.get("url") for s in schema.get("servers", [])}
if root_path not in server_urls:
schema = dict(schema)
schema["servers"] = [{"url": root_path}] + schema.get("servers", [])
return JSONResponse(schema)
Disabling Automatic Server Injection
If you prefer to define your servers list manually and do not want FastAPI to inject the root_path, you can set root_path_in_servers=False in the FastAPI constructor. This is useful when your API is served from multiple domains or complex environments where a single root_path is insufficient.
Design Tradeoffs and Constraints
It is important to note that root_path does not change internal routing.
If you define a route as @app.get("/items"), FastAPI will still match that route against the internal path /items. The root_path is purely metadata used for generating external-facing URLs (OpenAPI, Swagger UI, ReDoc). This design choice keeps the application logic decoupled from the deployment environment; you can move the same application from /api/v1 to /api/v2 just by changing the proxy configuration and the root_path parameter, without touching your path operation decorators.
Testing Proxy Configurations
You can verify your proxy configuration using the TestClient. By passing a root_path to the client, you simulate the behavior of an ASGI server running behind a proxy.
The following example (adapted from tests/test_openapi_cache_root_path.py) demonstrates how to verify that the root_path correctly appears in the OpenAPI schema:
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
def read_root():
return {"ok": True}
def test_root_path_injection():
client = TestClient(app, root_path="/api/v1")
response = client.get("/openapi.json")
data = response.json()
# Verify the root_path was injected into the servers list
servers = [s.get("url") for s in data.get("servers", [])]
assert "/api/v1" in servers
FastAPI ensures that these modifications are not persisted in the cached app.openapi_schema attribute, preventing root_path values from leaking between different requests or clients.