diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index d51c532b1a..ef962d5424 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -40,6 +40,15 @@ $ fastapi run --forwarded-allow-ips="*" +/// note + +The default value for the `--forwarded-allow-ips` option is `127.0.0.1`. + +This means that your **server** will trust a **proxy** running on the same host and will accept headers added by that **proxy**. + +/// + + ### Redirects with HTTPS { #redirects-with-https } For example, let's say you define a *path operation* `/items/`: @@ -64,7 +73,7 @@ If you want to learn more about HTTPS, check the guide [About HTTPS](../deployme /// -### How Proxy Forwarded Headers Work +### How Proxy Forwarded Headers Work { #how-proxy-forwarded-headers-work } Here's a visual representation of how the **proxy** adds forwarded headers between the client and the **application server**: @@ -97,7 +106,97 @@ These headers preserve information about the original request that would otherwi When **FastAPI CLI** is configured with `--forwarded-allow-ips`, it trusts these headers and uses them, for example to generate the correct URLs in redirects. -## Serve the app under a path prefix + + +## Testing locally with Traefik { #testing-locally-with-traefik } + +You can easily run the configuration with reverse proxy and ASGI application behind it locally using Traefik. + +Download Traefik, it's a single binary, you can extract the compressed file and run it directly from the terminal. + +Then create a file `traefik.toml` with: + +```TOML hl_lines="3" +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" +``` + +This tells Traefik to listen on port 9999 and to use another file `routes.toml`. + +/// tip + +We are using port 9999 instead of the standard HTTP port 80 so that you don't have to run it with admin (`sudo`) privileges. + +/// + +Now create that other file `routes.toml`: + +```TOML hl_lines="8 15" +[http] + + [http.routers] + + [http.routers.app-http] + entryPoints = ["http"] + service = "app" + rule = "PathPrefix(`/`)" + + [http.services] + + [http.services.app] + [http.services.app.loadBalancer] + [[http.services.app.loadBalancer.servers]] + url = "http://127.0.0.1:8000" +``` + +This file configures Traefik to forward all requests (``rule = "PathPrefix(`/`)"``) to your Uvicorn running on `http://127.0.0.1:8000`. + +Now start Traefik: + +
-At this point, it becomes clear that we need to tell the app the path prefix on which it's running.
+At this point, it becomes clear that we need to tell the app the path prefix on which it's running. So that it could use this prefix to create working URLs.
Luckily, this problem isn't new - and the people who designed the ASGI specification have already thought about it.
-### Understanding `root_path` in ASGI
+### Understanding `root_path` in ASGI { #understanding-root-path-in-asgi }
ASGI defines two fields that make it possible for applications to know where they are mounted and still handle requests correctly:
@@ -237,14 +368,14 @@ With this information, the application always knows both:
For example, if the client requests:
```
-/api/v1/app
+/api/v1/items/
```
the ASGI server should pass this to the app as:
```
{
- "path": "/api/v1/app",
+ "path": "/api/v1/items/",
"root_path": "/api/v1",
...
}
@@ -252,12 +383,12 @@ the ASGI server should pass this to the app as:
This allows the app to:
-* Match routes correctly (`/app` inside the app).
-* Generate proper URLs and redirects that include the prefix (`/api/v1/app`).
+* Match routes correctly (`/items/` inside the app).
+* Generate proper URLs and redirects that include the prefix (`/api/v1/items/`).
This is the elegant mechanism that makes it possible for ASGI applications - including FastAPI - to work smoothly behind reverse proxies or under nested paths without needing to rewrite routes manually.
-### Providing the `root_path`
+### Providing the `root_path` { #providing-the-root-path }
So, the ASGI scope needs to contain the correct `path` and `root_path`.
But... who is actually responsible for setting them? 🤔
@@ -278,7 +409,11 @@ Let's look at each of these approaches in detail.
#### Uvicorn `--root-path` (proxy strips prefix) { #uvicorn-root-path-proxy-strips-prefix }
-If your proxy removes the prefix before forwarding requests, you should use the `--root-path` option of your ASGI server:
+Let's now use the following app:
+
+{* ../../docs_src/behind_a_proxy/tutorial001.py *}
+
+If your proxy removes the prefix before forwarding requests (like in Traefik configuration from [Stripping the prefix](#stripping-the-prefix){.internal-link target=_blank}), you should use the `--root-path` option of your ASGI server:
+#### Important note { #important-note }
-But if we access the docs UI at the "official" URL using the proxy with port `9999`, at `/api/v1/docs`, it works correctly! 🎉
+Of course, the idea here is that everyone would access the app through the proxy (`http://192.168.0.1:9999` in our configuration), and the the URLs requested by user would contain the path prefix `/api/v1`.
-You can check it at http://127.0.0.1:9999/api/v1/docs:
+And URLs 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.
-
+It's quite common mistake that people configure the ASGI server with `--root-path` option and then attempt to access it directly (without reverse proxy).
-Right as we wanted it. ✔️
+When `root_path` is configured (using any of the methods described above), always make sure that:
-This is because FastAPI uses this `root_path` to create the default `server` in OpenAPI with the URL provided by `root_path`.
+* ✅ You are accessing your server via revers proxy
+* ✅ URL includes prefix
## Additional servers { #additional-servers }
diff --git a/docs/en/docs/img/tutorial/behind-a-proxy/image01.png b/docs/en/docs/img/tutorial/behind-a-proxy/image01.png
index 8012031401..17216e3025 100644
Binary files a/docs/en/docs/img/tutorial/behind-a-proxy/image01.png and b/docs/en/docs/img/tutorial/behind-a-proxy/image01.png differ
diff --git a/docs_src/behind_a_proxy/tutorial001.py b/docs_src/behind_a_proxy/tutorial001.py
index ede59ada11..406dc477f1 100644
--- a/docs_src/behind_a_proxy/tutorial001.py
+++ b/docs_src/behind_a_proxy/tutorial001.py
@@ -5,4 +5,8 @@ app = FastAPI()
@app.get("/app")
def read_main(request: Request):
- return {"message": "Hello World", "root_path": request.scope.get("root_path")}
+ return {
+ "message": "Hello World",
+ "path": request.scope.get("path"),
+ "root_path": request.scope.get("root_path"),
+ }
diff --git a/docs_src/behind_a_proxy/tutorial002.py b/docs_src/behind_a_proxy/tutorial002.py
index c1600cde9e..caaaf2dcb5 100644
--- a/docs_src/behind_a_proxy/tutorial002.py
+++ b/docs_src/behind_a_proxy/tutorial002.py
@@ -5,4 +5,8 @@ app = FastAPI(root_path="/api/v1")
@app.get("/app")
def read_main(request: Request):
- return {"message": "Hello World", "root_path": request.scope.get("root_path")}
+ return {
+ "message": "Hello World",
+ "path": request.scope.get("path"),
+ "root_path": request.scope.get("root_path"),
+ }
diff --git a/docs_src/behind_a_proxy/tutorial005.py b/docs_src/behind_a_proxy/tutorial005.py
index 6548d4d7b6..91f9e803f5 100644
--- a/docs_src/behind_a_proxy/tutorial005.py
+++ b/docs_src/behind_a_proxy/tutorial005.py
@@ -24,4 +24,8 @@ app.add_middleware(ForwardedPrefixMiddleware)
@app.get("/app")
def read_main(request: Request):
- return {"message": "Hello World", "root_path": request.scope.get("root_path")}
+ return {
+ "message": "Hello World",
+ "path": request.scope.get("path"),
+ "root_path": request.scope.get("root_path"),
+ }
diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py
index a070f850f7..770caaa87e 100644
--- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py
+++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py
@@ -2,13 +2,17 @@ from fastapi.testclient import TestClient
from docs_src.behind_a_proxy.tutorial001 import app
-client = TestClient(app, root_path="/api/v1")
+client = TestClient(app, base_url="http://example.com/api/v1", root_path="/api/v1")
def test_main():
response = client.get("/app")
assert response.status_code == 200
- assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
+ assert response.json() == {
+ "message": "Hello World",
+ "path": "/api/v1/app",
+ "root_path": "/api/v1"
+ }
def test_openapi():
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 ce791e2157..89742146b0 100644
--- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py
+++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py
@@ -6,9 +6,13 @@ client = TestClient(app)
def test_main():
- response = client.get("/app")
+ response = client.get("/api/v1/app")
assert response.status_code == 200
- assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
+ assert response.json() == {
+ "message": "Hello World",
+ "path": "/api/v1/app",
+ "root_path": "/api/v1",
+ }
def test_openapi():
diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial005.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial005.py
index 1bc8504d0f..7240e9db80 100644
--- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial005.py
+++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial005.py
@@ -26,17 +26,25 @@ def test_forwarded_prefix_middleware(
headers["x-forwarded-prefix"] = root_path
response = client.get(requested_path, headers=headers)
assert response.status_code == 200
+ assert response.json()["path"] == requested_path
assert response.json()["root_path"] == expected_root_path
-def test_openapi_servers():
+@pytest.mark.parametrize(
+ "prefix",
+ [
+ "/api/v1",
+ pytest.param("/backend/v1", marks=pytest.mark.xfail),
+ ],
+)
+def test_openapi_servers(prefix: str):
client = TestClient(app)
- headers = {"x-forwarded-prefix": "/api/v1"}
- response = client.get("/api/v1/openapi.json", headers=headers)
+ headers = {"x-forwarded-prefix": f"{prefix}"}
+ response = client.get(f"{prefix}/openapi.json", headers=headers)
assert response.status_code == 200
openapi_data = response.json()
assert "servers" in openapi_data
- assert openapi_data["servers"] == [{"url": "/api/v1"}]
+ assert openapi_data["servers"] == [{"url": prefix}]
@pytest.mark.parametrize(