diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 34edd716b2..3df48d6013 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -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="*" - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -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 - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` -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:
```console $ uv run fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ```
@@ -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 - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` ### 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. diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5b5a3a8766..c9cca6f582 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -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 diff --git a/docs_src/behind_a_proxy/tutorial003_py310.py b/docs_src/behind_a_proxy/tutorial003_py310.py index 3b7d8be018..552cf04c0b 100644 --- a/docs_src/behind_a_proxy/tutorial003_py310.py +++ b/docs_src/behind_a_proxy/tutorial003_py310.py @@ -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", ) diff --git a/docs_src/behind_a_proxy/tutorial004_py310.py b/docs_src/behind_a_proxy/tutorial004_py310.py index 51bd5babc1..878e2a5dbe 100644 --- a/docs_src/behind_a_proxy/tutorial004_py310.py +++ b/docs_src/behind_a_proxy/tutorial004_py310.py @@ -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, ) diff --git a/fastapi/applications.py b/fastapi/applications.py index 65bb5078c7..07790d591f 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -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, diff --git a/tests/test_telemetry/test_http.py b/tests/test_telemetry/test_http.py index 3abeb06e24..45733218d7 100644 --- a/tests/test_telemetry/test_http.py +++ b/tests/test_telemetry/test_http.py @@ -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 ( diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py index c9356a5130..43b9297d0a 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -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( diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index d74da5b73e..08a0ad7c6f 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -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(): diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index f5e27c1183..058e412e0a 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -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():