mirror of https://github.com/tiangolo/fastapi.git
Update the part of "Behind a proxy" section related to `root_path`
This commit is contained in:
parent
78c94c3f56
commit
f23fba1ead
|
|
@ -97,19 +97,52 @@ 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.
|
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.
|
||||||
|
|
||||||
## Proxy with a stripped path prefix { #proxy-with-a-stripped-path-prefix }
|
## Serve the app under a path prefix
|
||||||
|
|
||||||
You could have a proxy that adds a path prefix to your application.
|
In production, you might want to serve your FastAPI application under a URL prefix, such as:
|
||||||
|
|
||||||
In these cases you can use `root_path` to configure your application.
|
```
|
||||||
|
https://example.com/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
The `root_path` is a mechanism provided by the ASGI specification (that FastAPI is built on, through Starlette).
|
instead of serving it directly at the root of the domain.
|
||||||
|
|
||||||
The `root_path` is used to handle these specific cases.
|
### Why serve under a prefix?
|
||||||
|
|
||||||
And it's also used internally when mounting sub-applications.
|
This is common when:
|
||||||
|
|
||||||
Having a proxy with a stripped path prefix, in this case, means that you could declare a path at `/app` in your code, but then, you add a layer on top (the proxy) that would put your **FastAPI** application under a path like `/api/v1`.
|
* You host several applications on the same host and port - for example, an API, a dashboard, and an admin panel, all behind the same domain.
|
||||||
|
Serving them under different prefixes (like `/api`, `/dashboard`, and `/admin`) helps you avoid setting up multiple domains or ports and prevents cross-origin (CORS) issues.
|
||||||
|
* You deploy multiple API versions (e.g. `/v1`, `/v2`) side by side to ensure backward compatibility while rolling out new features.
|
||||||
|
|
||||||
|
Hosting under a prefix keeps everything accessible under a single base URL (like `https://example.com`), simplifying proxy, SSL, and frontend configuration.
|
||||||
|
|
||||||
|
### Routing requests through a reverse proxy
|
||||||
|
|
||||||
|
In these setups, you typically run your FastAPI app behind a reverse proxy such as Traefik, Nginx, Caddy, or an API Gateway.
|
||||||
|
The proxy is configured to route requests under a specific path prefix.
|
||||||
|
|
||||||
|
For example, using a Traefik router:
|
||||||
|
|
||||||
|
```TOML hl_lines="8"
|
||||||
|
[http]
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
|
||||||
|
[http.routers.app-http]
|
||||||
|
entryPoints = ["http"]
|
||||||
|
service = "app"
|
||||||
|
rule = "PathPrefix(`/api/v1`)"
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
|
||||||
|
[http.services.app]
|
||||||
|
[http.services.app.loadBalancer]
|
||||||
|
[[http.services.app.loadBalancer.servers]]
|
||||||
|
url = "http://127.0.0.1:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
This router config tells Traefik to forward all requests starting with `/api/v1` to your FastAPI service.
|
||||||
|
|
||||||
In this case, the original path `/app` would actually be served at `/api/v1/app`.
|
In this case, the original path `/app` would actually be served at `/api/v1/app`.
|
||||||
|
|
||||||
|
|
@ -117,15 +150,56 @@ Even though all your code is written assuming there's just `/app`.
|
||||||
|
|
||||||
{* ../../docs_src/behind_a_proxy/tutorial001.py hl[6] *}
|
{* ../../docs_src/behind_a_proxy/tutorial001.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`.
|
...but your app only knows routes like `/app` and has no idea about the `/api/v1` prefix.
|
||||||
|
|
||||||
Up to here, everything would work as normally.
|
As a result, the app won't be able to match the routes correctly, and clients will get `404 Not Found` errors 😱.
|
||||||
|
|
||||||
But then, when you open the integrated docs UI (the frontend), it would expect to get the OpenAPI schema at `/openapi.json`, instead of `/api/v1/openapi.json`.
|
### Stripping the prefix
|
||||||
|
|
||||||
So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema.
|
One simple way to solve the problem described above is to configure the proxy to strip the prefix before forwarding the request to the app.
|
||||||
|
For example, if the client requests `/api/v1/app`, the proxy forwards it as just `/app`.
|
||||||
|
|
||||||
Because we have a proxy with a path prefix of `/api/v1` for our app, the frontend needs to fetch the OpenAPI schema at `/api/v1/openapi.json`.
|
```TOML hl_lines="2-5 13"
|
||||||
|
[http]
|
||||||
|
[http.middlewares]
|
||||||
|
|
||||||
|
[http.middlewares.api-stripprefix.stripPrefix]
|
||||||
|
prefixes = ["/api/v1"]
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
|
||||||
|
[http.routers.app-http]
|
||||||
|
entryPoints = ["http"]
|
||||||
|
service = "app"
|
||||||
|
rule = "PathPrefix(`/api/v1`)"
|
||||||
|
middlewares = ["api-stripprefix"]
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
|
||||||
|
[http.services.app]
|
||||||
|
[http.services.app.loadBalancer]
|
||||||
|
[[http.services.app.loadBalancer.servers]]
|
||||||
|
url = "http://127.0.0.1:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
Now the app's routing works perfectly - `/app` matches exactly as expected. 🎉
|
||||||
|
|
||||||
|
But... something is still off...
|
||||||
|
|
||||||
|
### When the app doesn't know it's running under a prefix
|
||||||
|
|
||||||
|
While stripping the prefix by the proxy fixes incoming requests, the app still doesn't know that it's being served under `/api/v1`.
|
||||||
|
So any URLs generated inside the app - like those from `url_for()`, URL of `openapi.json` in docs, or redirects - will miss the prefix.
|
||||||
|
Clients will get links like `/app` instead of `/api/v1/app`, breaking navigation and documentation.
|
||||||
|
|
||||||
|
Let's look closer at the problem with URL of `openapi.json` in docs.
|
||||||
|
|
||||||
|
When you open the integrated docs UI (the frontend), it expects to get the OpenAPI schema at `/openapi.json`.
|
||||||
|
But, since your app is served under the `/api/v1` prefix, the correct URL of `openapi.json` would be `/api/v1/openapi.json`.
|
||||||
|
|
||||||
|
So the frontend, running in the browser, will try to reach `/openapi.json` and fail to get the OpenAPI schema.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph LR
|
graph LR
|
||||||
|
|
@ -144,28 +218,67 @@ The IP `0.0.0.0` is commonly used to mean that the program listens on all the IP
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
||||||
The docs UI would also need the OpenAPI schema to declare that this API `server` is located at `/api/v1` (behind the proxy). For example:
|
At this point, it becomes clear that we need to tell the app the path prefix on which it's running.
|
||||||
|
|
||||||
```JSON hl_lines="4-8"
|
Luckily, this problem isn't new - and the people who designed the ASGI specification have already thought about it.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
* `path` – the full path requested by the client (including the prefix)
|
||||||
|
* `root_path` – the mount point (the prefix itself) under which the app is served
|
||||||
|
|
||||||
|
With this information, the application always knows both:
|
||||||
|
|
||||||
|
* what the user actually requested (`path`), and
|
||||||
|
* where the app lives in the larger URL structure (`root_path`).
|
||||||
|
|
||||||
|
For example, if the client requests:
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/v1/app
|
||||||
|
```
|
||||||
|
|
||||||
|
the ASGI server should pass this to the app as:
|
||||||
|
|
||||||
|
```
|
||||||
{
|
{
|
||||||
"openapi": "3.1.0",
|
"path": "/api/v1/app",
|
||||||
// More stuff here
|
"root_path": "/api/v1",
|
||||||
"servers": [
|
...
|
||||||
{
|
|
||||||
"url": "/api/v1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
// More stuff here
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
This allows the app to:
|
||||||
|
|
||||||
### Providing the `root_path` { #providing-the-root-path }
|
* Match routes correctly (`/app` inside the app).
|
||||||
|
* Generate proper URLs and redirects that include the prefix (`/api/v1/app`).
|
||||||
|
|
||||||
To achieve this, you can use the command line option `--root-path` like:
|
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`
|
||||||
|
|
||||||
|
So, the ASGI scope needs to contain the correct `path` and `root_path`.
|
||||||
|
But... who is actually responsible for setting them? 🤔
|
||||||
|
|
||||||
|
As you may recall, there are three main components involved in serving your FastAPI application:
|
||||||
|
|
||||||
|
* **Reverse proxy** (like Traefik or Nginx) - receives client requests first and passes them to the ASGI server.
|
||||||
|
* **ASGI server** (like Uvicorn or Hypercorn) - runs your FastAPI application and manages the ASGI lifecycle.
|
||||||
|
* **ASGI app** itself (the app you're building with FastAPI, your favorite framework) - handles routing, generates URLs, and processes requests.
|
||||||
|
|
||||||
|
There are a few common ways these components can work together to ensure `root_path` is set correctly:
|
||||||
|
|
||||||
|
1. The proxy strips the prefix, and the ASGI server (e.g., Uvicorn) adds it back and sets `root_path` in the ASGI `scope`.
|
||||||
|
2. The proxy keeps the prefix and forwards requests as-is, while the ASGI app is started with the correct `root_path` parameter.
|
||||||
|
3. The proxy keeps the prefix but also sends an `X-Forwarded-Prefix` header, and a middleware in the app uses that header to dynamically set `root_path`.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
<div class="termy">
|
<div class="termy">
|
||||||
|
|
||||||
|
|
@ -179,23 +292,28 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||||
|
|
||||||
If you use Hypercorn, it also has the option `--root-path`.
|
If you use Hypercorn, it also has the option `--root-path`.
|
||||||
|
|
||||||
/// note | Technical Details
|
Here's what happens in this setup:
|
||||||
|
|
||||||
The ASGI specification defines a `root_path` for this use case.
|
* The proxy sends `/app` to the ASGI server (Uvicorn).
|
||||||
|
* Uvicorn adds the prefix to `path` and sets `root_path` to `/api/v1` in the ASGI scope.
|
||||||
|
* Your FastAPI app receives:
|
||||||
|
|
||||||
And the `--root-path` command line option provides that `root_path`.
|
```
|
||||||
|
{
|
||||||
|
"path": "/api/v1/app",
|
||||||
|
"root_path": "/api/v1",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
///
|
✅ This is fully compliant with the ASGI specification.
|
||||||
|
|
||||||
### Checking the current `root_path` { #checking-the-current-root-path }
|
FastAPI automatically respects `root_path` in the scope when matching routes or generating URLs.
|
||||||
|
|
||||||
You can get the current `root_path` used by your application for each request, it is part of the `scope` dictionary (that's part of the ASGI spec).
|
Run the following example app:
|
||||||
|
|
||||||
Here we are including it in the message just for demonstration purposes.
|
|
||||||
|
|
||||||
{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *}
|
{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *}
|
||||||
|
|
||||||
Then, if you start Uvicorn with:
|
with the command:
|
||||||
|
|
||||||
<div class="termy">
|
<div class="termy">
|
||||||
|
|
||||||
|
|
@ -207,7 +325,7 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
The response would be something like:
|
The response will look something like this:
|
||||||
|
|
||||||
```JSON
|
```JSON
|
||||||
{
|
{
|
||||||
|
|
@ -216,38 +334,187 @@ The response would be something like:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setting the `root_path` in the FastAPI app { #setting-the-root-path-in-the-fastapi-app }
|
/// warning | Attention
|
||||||
|
|
||||||
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:
|
Don't forget - this setup assumes your proxy **removes** the `/api/v1` prefix before forwarding requests to the app.
|
||||||
|
|
||||||
|
If the proxy **doesn't strip** the prefix, you will end up with duplicated paths like `/api/v1/api/v1/app`.
|
||||||
|
|
||||||
|
And if you run the server **without a proxy at all**, your app will still handle requests to URLs like `/app`,
|
||||||
|
but URL generation, redirects, and the interactive docs will break due to the missing prefix configuration.
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
/// tip
|
||||||
|
|
||||||
|
This is the most straightforward and common approach.
|
||||||
|
You should probably use it unless you have a specific reason to do otherwise.
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
Unfortunately, not all ASGI servers support a `--root-path` option or automatically adjust the `path` in the ASGI scope.
|
||||||
|
If your server doesn't, you can use one of the alternative approaches described below.
|
||||||
|
|
||||||
|
#### Passing `root_path` as an argument to FastAPI (proxy keeps prefix)
|
||||||
|
|
||||||
|
If the proxy keeps the prefix in the forwarded request:
|
||||||
|
|
||||||
|
```TOML
|
||||||
|
[http]
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
|
||||||
|
[http.routers.app-http]
|
||||||
|
entryPoints = ["http"]
|
||||||
|
service = "app"
|
||||||
|
rule = "PathPrefix(`/api/v1`)"
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
|
||||||
|
[http.services.app]
|
||||||
|
[http.services.app.loadBalancer]
|
||||||
|
[[http.services.app.loadBalancer.servers]]
|
||||||
|
url = "http://127.0.0.1:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
you can configure FastAPI like this:
|
||||||
|
|
||||||
{* ../../docs_src/behind_a_proxy/tutorial002.py hl[3] *}
|
{* ../../docs_src/behind_a_proxy/tutorial002.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.
|
In this setup:
|
||||||
|
|
||||||
### About `root_path` { #about-root-path }
|
The app receives requests with `/api/v1` included in the path.
|
||||||
|
|
||||||
Keep in mind that the server (Uvicorn) won't use that `root_path` for anything else than passing it to the app.
|
FastAPI uses the `root_path` parameter to adjust the ASGI scope so that routing, redirects, and URL generation work correctly.
|
||||||
|
|
||||||
But if you go with your browser to <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000/app</a> you will see the normal response:
|
Without the `root_path` parameter, the incoming scope from the ASGI server looks like this:
|
||||||
|
|
||||||
```JSON
|
```
|
||||||
{
|
{
|
||||||
"message": "Hello World",
|
"path": "/api/v1/app",
|
||||||
"root_path": "/api/v1"
|
"root_path": "",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
So, it won't expect to be accessed at `http://127.0.0.1:8000/api/v1/app`.
|
This scope is not compliant with the ASGI specification.
|
||||||
|
|
||||||
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.
|
But thanks to the `root_path` parameter, FastAPI corrects the scope to:
|
||||||
|
|
||||||
## About proxies with a stripped path prefix { #about-proxies-with-a-stripped-path-prefix }
|
```
|
||||||
|
{
|
||||||
|
"path": "/api/v1/app",
|
||||||
|
"root_path": "/api/v1",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Keep in mind that a proxy with stripped path prefix is only one of the ways to configure it.
|
/// warning | Attention
|
||||||
|
|
||||||
Probably in many cases the default will be that the proxy doesn't have a stripped path prefix.
|
Don't forget - this setup assumes your app runs behind a proxy that **keeps (adds)** the `/api/v1` prefix when forwarding requests.
|
||||||
|
|
||||||
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`.
|
If the proxy **strips the prefix** or **doesn't add it at all**, your app might seem to work for some routes, but features like interactive docs, mounted sub-applications, and redirects will fail.
|
||||||
|
|
||||||
|
If you encounter strange issues with this configuration, double-check your proxy settings.
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
/// note
|
||||||
|
|
||||||
|
Use this approach when your ASGI server doesn't support a `--root-path` option, or if you need to configure your reverse proxy to keep the prefix in the path.
|
||||||
|
|
||||||
|
Otherwise, prefer using the `--root-path` approach [described above](#uvicorn-root-path-proxy-strips-prefix){.internal-link target=_blank}.
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
#### Using `X-Forwarded-Prefix` header
|
||||||
|
|
||||||
|
This is another common approach. It's the most flexible, but requires a more initial configuration.
|
||||||
|
|
||||||
|
/// warning
|
||||||
|
|
||||||
|
This is a more advanced approach. In most cases, you should use the `--root-path` option [described above](#uvicorn-root-path-proxy-strips-prefix){.internal-link target=_blank}.
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
Imagine the prefix you use to serve the app might change over time. With the `--root-path` approach, you would need to update both the proxy configuration and the ASGI server command each time the prefix changes.
|
||||||
|
|
||||||
|
Or, suppose you want to serve your app under multiple prefixes, such as `/api/v1` and `/backend/v1`. You would then need multiple instances of your app configured with different `--root-path` values - not ideal.
|
||||||
|
|
||||||
|
There is a better solution for this! 💡
|
||||||
|
Configure your reverse proxy to send the mount prefix in a header, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Forwarded-Prefix: /api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is an example Traefik configuration:
|
||||||
|
|
||||||
|
```TOML
|
||||||
|
[http]
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
|
||||||
|
[http.routers.app-api-v1]
|
||||||
|
entryPoints = ["http"]
|
||||||
|
service = "app"
|
||||||
|
rule = "PathPrefix(`/api/v1`)"
|
||||||
|
middlewares = ["prefix-api-v1"]
|
||||||
|
|
||||||
|
[http.routers.app-backend-v1]
|
||||||
|
entryPoints = ["http"]
|
||||||
|
service = "app"
|
||||||
|
rule = "PathPrefix(`/backend/v1`)"
|
||||||
|
middlewares = ["prefix-backend-v1"]
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
|
||||||
|
[http.services.app.loadBalancer]
|
||||||
|
[[http.services.app.loadBalancer.servers]]
|
||||||
|
url = "http://127.0.0.1:8000"
|
||||||
|
|
||||||
|
[http.middlewares]
|
||||||
|
|
||||||
|
[http.middlewares.prefix-api-v1.headers.customRequestHeaders]
|
||||||
|
X-Forwarded-Prefix = "/api/v1"
|
||||||
|
|
||||||
|
[http.middlewares.prefix-backend-v1.headers.customRequestHeaders]
|
||||||
|
X-Forwarded-Prefix = "/backend/v1"
|
||||||
|
```
|
||||||
|
|
||||||
|
/// note
|
||||||
|
|
||||||
|
`X-Forwarded-Prefix` is not a part of any standard, but it is widely recognized for informing an app of its URL path prefix.
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
You can then use middleware to read this header and dynamically set the correct `root_path`:
|
||||||
|
|
||||||
|
{* ../../docs_src/behind_a_proxy/tutorial005.py *}
|
||||||
|
|
||||||
|
This allows a single FastAPI instance to handle requests under multiple prefixes, with `root_path` correctly set for each request.
|
||||||
|
|
||||||
|
Run the server:
|
||||||
|
|
||||||
|
<div class="termy">
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ fastapi run main.py --forwarded-allow-ips="*"
|
||||||
|
|
||||||
|
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Test with `curl`:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -H "x-forwarded-prefix: /api/v1" http://127.0.0.1:8000/api/v1/app
|
||||||
|
# {"message":"Hello World","root_path":"/api/v1"}
|
||||||
|
|
||||||
|
curl -H "x-forwarded-prefix: /backend/v1" http://127.0.0.1:8000/backend/v1/app
|
||||||
|
# {"message":"Hello World","root_path":"/backend/v1"}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same FastAPI instance now handles requests for multiple prefixes, using the correct `root_path` each time.
|
||||||
|
|
||||||
## Testing locally with Traefik { #testing-locally-with-traefik }
|
## Testing locally with Traefik { #testing-locally-with-traefik }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardedPrefixMiddleware:
|
||||||
|
def __init__(self, app: ASGIApp):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
||||||
|
if scope["type"] in ("http", "websocket"):
|
||||||
|
scope_headers: list[tuple[bytes, bytes]] = scope.get("headers", [])
|
||||||
|
headers = {
|
||||||
|
k.decode("latin-1"): v.decode("latin-1") for k, v in scope_headers
|
||||||
|
}
|
||||||
|
prefix = headers.get("x-forwarded-prefix", "").rstrip("/")
|
||||||
|
if prefix:
|
||||||
|
scope["root_path"] = prefix
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.add_middleware(ForwardedPrefixMiddleware)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/app")
|
||||||
|
def read_main(request: Request):
|
||||||
|
return {"message": "Hello World", "root_path": request.scope.get("root_path")}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from docs_src.behind_a_proxy.tutorial005 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"root_path, requested_path, expected_root_path",
|
||||||
|
[
|
||||||
|
("/api/v1", "/api/v1/app", "/api/v1"),
|
||||||
|
("/backend/v1", "/backend/v1/app", "/backend/v1"),
|
||||||
|
("/backend/v1/", "/backend/v1/app", "/backend/v1"),
|
||||||
|
(None, "/app", ""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_forwarded_prefix_middleware(
|
||||||
|
root_path: str,
|
||||||
|
requested_path: str,
|
||||||
|
expected_root_path: str,
|
||||||
|
):
|
||||||
|
client = TestClient(app)
|
||||||
|
headers = {}
|
||||||
|
if root_path:
|
||||||
|
headers["x-forwarded-prefix"] = root_path
|
||||||
|
response = client.get(requested_path, headers=headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["root_path"] == expected_root_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi_servers():
|
||||||
|
client = TestClient(app)
|
||||||
|
headers = {"x-forwarded-prefix": "/api/v1"}
|
||||||
|
response = client.get("/api/v1/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"}]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"root_path, requested_path, expected_openapi_url",
|
||||||
|
[
|
||||||
|
("/api/v1", "/api/v1/docs", "/api/v1/openapi.json"),
|
||||||
|
(None, "/docs", "/openapi.json"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_swagger_docs_openapi_url(
|
||||||
|
root_path: str, requested_path: str, expected_openapi_url: str
|
||||||
|
):
|
||||||
|
client = TestClient(app)
|
||||||
|
headers = {}
|
||||||
|
if root_path:
|
||||||
|
headers["x-forwarded-prefix"] = root_path
|
||||||
|
response = client.get(requested_path, headers=headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert expected_openapi_url in response.text
|
||||||
Loading…
Reference in New Issue