diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index d51c532b1..ef962d542 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: + +
-The IP `0.0.0.0` is commonly used to mean that the program listens on all the IPs available in that machine/server.
-
-///
-
-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:
-
-But if we access the docs UI at the "official" URL using the proxy with port `9999`, at `/api/v1/docs`, it works correctly! 🎉
-
-You can check it at http://127.0.0.1:9999/api/v1/docs:
-
-
-
-Right as we wanted it. ✔️
-
-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 801203140..17216e302 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 ede59ada1..406dc477f 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 c1600cde9..caaaf2dcb 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 6548d4d7b..91f9e803f 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 a070f850f..770caaa87 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 ce791e215..89742146b 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 1bc8504d0..7240e9db8 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(