🗑️ Deprecate the root_path parameter in FastAPI (#16520)

This commit is contained in:
Sebastián Ramírez authored and GitHub committed 2026-10-18 17:07:43 +02:00
1 parent 129c9cfcd3
commit 45c4199384
9 files changed
+57 -67

No files matched your search

+16 -45
View File
@@ -8,7 +8,7 @@ These proxies could handle HTTPS certificates and other things.
A **proxy** in front of your application would normally set some headers on the fly before sending the requests to your **server** to let the server know that the request was **forwarded** by the proxy, letting it know the original (public) URL, including the domain, that it is using HTTPS, etc.
The **server** program (for example **Uvicorn** via **FastAPI CLI**) is capable of interpreting these headers, and then passing that information to your application.
The **server** started by `fastapi run` is capable of interpreting these headers, and then passing that information to your application.
But for security, as the server doesn't know it is behind a trusted proxy, it won't interpret those headers.
@@ -34,8 +34,6 @@ If your **server** is behind a trusted **proxy** and only the proxy talks to it,
```console
$ uv run fastapi run --forwarded-allow-ips="*"
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
</div>
@@ -101,7 +99,7 @@ When **FastAPI CLI** is configured with `--forwarded-allow-ips`, it trusts these
You could have a proxy that adds a path prefix to your application.
In these cases you can use `root_path` to configure your application.
In these cases you can configure the server with `fastapi run --root-path`.
The `root_path` is a mechanism provided by the ASGI specification (that FastAPI is built on, through Starlette).
@@ -117,7 +115,7 @@ Even though all your code is written assuming there's just `/app`.
{* ../../docs_src/behind_a_proxy/tutorial001_py310.py hl[6] *}
And the proxy would be **"stripping"** the **path prefix** on the fly before transmitting the request to the app server (probably Uvicorn via FastAPI CLI), keeping your application convinced that it is being served at `/app`, so that you don't have to update all your code to include the prefix `/api/v1`.
And the proxy would be **"stripping"** the **path prefix** on the fly before transmitting the request to the server, keeping your application convinced that it is being served at `/app`, so that you don't have to update all your code to include the prefix `/api/v1`.
Up to here, everything would work as normally.
@@ -161,7 +159,7 @@ The docs UI would also need the OpenAPI schema to declare that this API `server`
}
```
In this example, the "Proxy" could be something like **Traefik**. And the server would be something like FastAPI CLI with **Uvicorn**, running your FastAPI application.
In this example, the "Proxy" could be something like **Traefik**. And `fastapi run` would start the server running your FastAPI application.
### Providing the `root_path` { #providing-the-root-path }
@@ -171,14 +169,10 @@ To achieve this, you can use the command line option `--root-path` like:
```console
$ uv run fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
</div>
If you use Hypercorn, it also has the option `--root-path`.
/// note | Technical Details
The ASGI specification defines a `root_path` for this use case.
@@ -195,14 +189,12 @@ Here we are including it in the message just for demonstration purposes.
{* ../../docs_src/behind_a_proxy/tutorial001_py310.py hl[8] *}
Then, if you start Uvicorn with:
Then, if you start the server with:
<div class="termy">
```console
$ uv run fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
</div>
@@ -216,30 +208,11 @@ The response would be something like:
}
```
### Setting the `root_path` in the FastAPI app { #setting-the-root-path-in-the-fastapi-app }
/// warning | Deprecated `root_path` parameter
Alternatively, if you don't have a way to provide a command line option like `--root-path` or equivalent, you can set the `root_path` parameter when creating your FastAPI app:
In FastAPI 0.145.0 and earlier, `root_path` could also be configured in `FastAPI()`. This parameter is now deprecated because its behavior was not consistent with the server configuration. Use `fastapi run --root-path /api/v1` instead.
{* ../../docs_src/behind_a_proxy/tutorial002_py310.py hl[3] *}
Passing the `root_path` to `FastAPI` would be the equivalent of passing the `--root-path` command line option to Uvicorn or Hypercorn.
### About `root_path` { #about-root-path }
Keep in mind that the server (Uvicorn) won't use that `root_path` for anything else than passing it to the app.
But if you go with your browser to [http://127.0.0.1:8000/app](http://127.0.0.1:8000/app) you will see the normal response:
```JSON
{
"message": "Hello World",
"root_path": "/api/v1"
}
```
So, it won't expect to be accessed at `http://127.0.0.1:8000/api/v1/app`.
Uvicorn will expect the proxy to access Uvicorn at `http://127.0.0.1:8000/app`, and then it would be the proxy's responsibility to add the extra `/api/v1` prefix on top.
///
## About proxies with a stripped path prefix { #about-proxies-with-a-stripped-path-prefix }
@@ -247,7 +220,7 @@ Keep in mind that a proxy with stripped path prefix is only one of the ways to c
Probably in many cases the default will be that the proxy doesn't have a stripped path prefix.
In a case like that (without a stripped path prefix), the proxy would listen on something like `https://myawesomeapp.com`, and then if the browser goes to `https://myawesomeapp.com/api/v1/app` and your server (e.g. Uvicorn) listens on `http://127.0.0.1:8000` the proxy (without a stripped path prefix) would access Uvicorn at the same path: `http://127.0.0.1:8000/api/v1/app`.
In a case like that (without a stripped path prefix), the proxy would listen on something like `https://myawesomeapp.com`, and then if the browser goes to `https://myawesomeapp.com/api/v1/app` and your server listens on `http://127.0.0.1:8000` the proxy (without a stripped path prefix) would access the server at the same path: `http://127.0.0.1:8000/api/v1/app`.
## Testing locally with Traefik { #testing-locally-with-traefik }
@@ -302,7 +275,7 @@ Now create that other file `routes.toml`:
This file configures Traefik to use the path prefix `/api/v1`.
And then Traefik will redirect its requests to your Uvicorn running on `http://127.0.0.1:8000`.
And then Traefik will forward its requests to your server running on `http://127.0.0.1:8000`.
Now start Traefik:
@@ -322,15 +295,13 @@ And now start your app, using the `--root-path` option:
```console
$ uv run fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```
</div>
### Check the responses { #check-the-responses }
Now, if you go to the URL with the port for Uvicorn: [http://127.0.0.1:8000/app](http://127.0.0.1:8000/app), you will see the normal response:
Now, if you go to the URL with the port for the server: [http://127.0.0.1:8000/app](http://127.0.0.1:8000/app), you will see the normal response:
```JSON
{
@@ -360,15 +331,15 @@ but this time at the URL with the prefix path provided by the proxy: `/api/v1`.
Of course, the idea here is that everyone would access the app through the proxy, so the version with the path prefix `/api/v1` is the "correct" one.
And the version without the path prefix (`http://127.0.0.1:8000/app`), provided by Uvicorn directly, would be exclusively for the _proxy_ (Traefik) to access it.
And the version without the path prefix (`http://127.0.0.1:8000/app`), provided by the server directly, would be exclusively for the _proxy_ (Traefik) to access it.
That demonstrates how the Proxy (Traefik) uses the path prefix and how the server (Uvicorn) uses the `root_path` from the option `--root-path`.
That demonstrates how the Proxy (Traefik) uses the path prefix and how the server uses the `root_path` from the option `--root-path`.
### Check the docs UI { #check-the-docs-ui }
But here's the fun part. ✨
The "official" way to access the app would be through the proxy with the path prefix that we defined. So, as we would expect, if you try the docs UI served by Uvicorn directly, without the path prefix in the URL, it won't work, because it expects to be accessed through the proxy.
The "official" way to access the app would be through the proxy with the path prefix that we defined. So, as we would expect, if you try the docs UI served by the server directly, without the path prefix in the URL, it won't work, because it expects to be accessed through the proxy.
You can check it at [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs):
@@ -398,7 +369,7 @@ But you can also provide other alternative `servers`, for example if you want *t
If you pass a custom list of `servers` and there's a `root_path` (because your API lives behind a proxy), **FastAPI** will insert a "server" with this `root_path` at the beginning of the list.
For example:
For example, using the same `fastapi run --root-path /api/v1` server configuration:
{* ../../docs_src/behind_a_proxy/tutorial003_py310.py hl[4:7] *}
@@ -455,7 +426,7 @@ If you don't specify the `servers` parameter and `root_path` is equal to `/`, th
If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`:
{* ../../docs_src/behind_a_proxy/tutorial004_py310.py hl[9] *}
{* ../../docs_src/behind_a_proxy/tutorial004_py310.py hl[8] *}
and then it won't include it in the OpenAPI schema.
+2
View File
@@ -7,6 +7,8 @@ hide:
## Latest Changes
The `root_path` parameter in `FastAPI()` is deprecated. Configure the server with `fastapi run --root-path /api/v1` instead.
## 0.145.0 (2026-10-18)
### Breaking Changes
@@ -5,7 +5,6 @@ app = FastAPI(
{"url": "https://stag.example.com", "description": "Staging environment"},
{"url": "https://prod.example.com", "description": "Production environment"},
],
root_path="/api/v1",
)
@@ -5,7 +5,6 @@ app = FastAPI(
{"url": "https://stag.example.com", "description": "Staging environment"},
{"url": "https://prod.example.com", "description": "Production environment"},
],
root_path="/api/v1",
root_path_in_servers=False,
)
+24 -12
View File
@@ -1,4 +1,5 @@
import os
import warnings
from collections.abc import Awaitable, Callable, Coroutine, Sequence
from enum import Enum
from typing import Annotated, Any, Literal, TypeVar
@@ -11,7 +12,11 @@ from fastapi.exception_handlers import (
request_validation_exception_handler,
websocket_request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.exceptions import (
FastAPIDeprecationWarning,
RequestValidationError,
WebSocketRequestValidationError,
)
from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware
from fastapi.openapi.docs import (
get_redoc_html,
@@ -644,19 +649,18 @@ class FastAPI(Starlette):
str,
Doc(
"""
A path prefix handled by a proxy that is not seen by the application
but is seen by external clients, which affects things like Swagger UI.
Deprecated. Configure the server with
`fastapi run --root-path /api/v1` instead.
Read more about it at the
Read more in the
[FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/).
**Example**
```python
from fastapi import FastAPI
app = FastAPI(root_path="/api/v1")
```
"""
),
deprecated(
"""
The `root_path` parameter is deprecated. Configure the server's
root path instead, for example with
`fastapi run --root-path /api/v1`.
"""
),
] = "",
@@ -931,6 +935,14 @@ class FastAPI(Starlette):
"""
),
] = webhooks or routing.APIRouter()
if root_path:
warnings.warn(
"The `root_path` parameter is deprecated. Configure the "
"server's root path instead, for example with "
"`fastapi run --root-path /api/v1`.",
category=FastAPIDeprecationWarning,
stacklevel=2,
)
self.root_path = root_path
self.state: Annotated[
State,
+4 -2
View File
@@ -221,7 +221,7 @@ def test_custom_error_handler_sees_span(telemetry):
def test_route_prefix_and_dynamic_mount(telemetry):
config, exporter, reader = telemetry
parent = FastAPI(telemetry=config, root_path="/proxy")
parent = FastAPI(telemetry=config)
child = FastAPI()
router = APIRouter(prefix="/v1")
@@ -231,7 +231,9 @@ def test_route_prefix_and_dynamic_mount(telemetry):
child.include_router(router, prefix="/api")
parent.mount("/tenants/{tenant}", child)
response = TestClient(parent).get("/proxy/tenants/acme/api/v1/items/4")
response = TestClient(parent, root_path="/proxy").get(
"/proxy/tenants/acme/api/v1/items/4"
)
assert response.json() == 4
(span,) = server_spans(exporter)
assert (
@@ -1,18 +1,23 @@
import pytest
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from docs_src.behind_a_proxy.tutorial002_py310 import app
client = TestClient(app)
@pytest.fixture(name="client", scope="module")
def get_client() -> TestClient:
with pytest.warns(FastAPIDeprecationWarning, match="fastapi run --root-path"):
from docs_src.behind_a_proxy.tutorial002_py310 import app
return TestClient(app)
def test_main():
def test_main(client: TestClient):
response = client.get("/app")
assert response.status_code == 200
assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
def test_openapi():
def test_openapi(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == snapshot(
@@ -3,7 +3,7 @@ from inline_snapshot import snapshot
from docs_src.behind_a_proxy.tutorial003_py310 import app
client = TestClient(app)
client = TestClient(app, root_path="/api/v1")
def test_main():
@@ -3,7 +3,7 @@ from inline_snapshot import snapshot
from docs_src.behind_a_proxy.tutorial004_py310 import app
client = TestClient(app)
client = TestClient(app, root_path="/api/v1")
def test_main():