Fastapi: How to configure the root_url/hostname of the API service?

Created on 11 Sep 2020  路  10Comments  路  Source: tiangolo/fastapi

Description

I'm new to FastAPI and I've set up an API service with FastAPI in docker behind Traefik v2.2 proxy. Traefik is configured to use Let's Encrypt resolver to automatically deal with the SSL stuff for the hostname of this service so all the clients are talking with this API endpoint in https url e.g. https://foo.bar and the API service itself (the uvicorn server) is running in non-ssl mode. Everything works fine in normal cases.

However, in certain cases, the API endpoint will send out some redirect responses. Those redirect response is targeting to http://foo.bar which is a different hostname because of the wrong scheme. I have two guesses for this:

  1. the uvicorn server is running in non-ssl mode
  2. the request from traefik router to my service is tweaked to http request though the original request from client to traefik is https

The client receiving such a redirect response will not carry the Authorization Header by default because the hostname has changed (from https to http).

Per talk with the Traefik community, the suggestion is to configure the root_url or something similar of my service (fastapi here). However I was not able find this configuration (the only relevant one I could find is talking about root_path but not root_url). Hope I could get some help here. Thank you all in advance!

question

Most helpful comment

@Reapor-Yurnero

uvicorn handles trailing slash redirects by default, you can disable this by passing redirect_slashes=False to your APIRouter.

Given your proxy is communicating with uvicorn via HTTP, it will naturally redirect to the same scheme (http). As @SebastianLuebke mentioned, uvicorn* does honor the X-FORWARDED-PROTO if enabled. It is enabled by default, and will only populate this information for trusted clients, which are supplied to uvicorn via the command line, or an environment variable:

  --forwarded-allow-ips TEXT      Comma seperated list of IPs to trust with
                                  proxy headers. Defaults to the
                                  $FORWARDED_ALLOW_IPS environment variable if
                                  available, or '127.0.0.1'.

uvicorn internally uses its own ProxyHeadersMiddleware middleware.

The easiest approach is to ensure the X-FORWARDED-PROTO is set to https, and that your proxy's IP is given to uvicorn via CLI or env ($FORWARDED_ALLOW_IPS).

In the case of Traefik, you can enable X-FORWARDED-PROTO behaviour via the sslProxyHeaders middleware. For me, this was as simple as adding the following two docker labels to my API container:

- traefik.http.middlewares.backend.headers.sslProxyHeaders.X-FORWARDED-PROTO=https
- traefik.http.routers.backend.middlewares=backend

One issue I personally had using Docker was passing the proxy IP to uvicorn. Using an environment variable in my docker-compose wasn't ideal, as it'd have to resolve the proxy IP by name dynamically (as uvicorn simply reads this as a string, for string comparison to the client address).

One solution to this would be setting FORWARDED_ALLOW_IPS=*, allowing uvicorn to respect any clients headers, although, perhaps not the most ideal situation, depending on your network setup.

Alternatively, you can add the ProxyHeadersMiddleware yourself via FastAPI. It takes the trusted_hosts in its ctor, meaning you can resolve your proxy IP at runtime, e.g:

from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
...
proxy_ip = socket.gethostbyname("proxy-container-name")
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=proxy_ip)

All 10 comments

Just create a SERVER_HOST (or root_url) in your pydantic settings and use that to generate your redirects.

from pydantic import BaseSettings
from fastapi import FastAPI
from fastapi.responses import RedirectResponse

class Settings(BaseSettings):
    SERVER_HOST: str

settings = Settings(SERVER_HOST="http://google.de")

app = FastAPI()

@app.get("/redirect")
def redirect():
    return RedirectResponse(f"{settings.SERVER_HOST}/")

@SebastianLuebke

Just create a SERVER_HOST (or root_url) in your pydantic settings and use that to generate your redirects.

Thanks for your help. But some redirect response is caused by the automatic trailing slash handling of uvicorn. How should I deal with them?

The automatic response is a relative redirect i think. so this should just work. enable http to https rewrite in your proxy config

i use nginx for that
http gets redirected to https
https proxies the requests to the http uvicorn server.

@SebastianLuebke Thanks for your response. But I don't understand the second half. If the request is proxyed to http uvicorn server,shouldn't the request become http again?

My current setup is similar to yours, all http requests to my traefik proxy is redirected to be https, but the traefik router itself is talking without ssl to my server and cause the request the server received to be http. And thus the redirect response is http.

That doesnt matter in this setup. even if the client gets a http redirect response from your uvicorn server, traefik will convert it to a https request or the browser does it for you if you use HTTP Strict Transport Security.

  1. A Client makes a request to traefik http
  2. then the client gets redirected to https
  3. traefik proxies the request to the http uvicorn server
  4. uvicorn (fastapi) sends a http redirect
  5. traefik redirects the http request to https or the browser translates it if you use HSTS.

@SebastianLuebke Thanks for your comprehensive explanation.

I understand what you are saying there. But the problem is: my client is sending request to traefik https in the first step, as I mentioned in the post. That's why there would be a hostname change and the authorization header will be dropped. I guess in most cases, we don't want to set the destination url from client to be http if https is possible, though I know let the clients to talk with http instead of https would immediately resolve the problem as you said there.

Is it possible to leave clients talking with https through router, but fastapi still response with redirect to https?

Try starting uvicorn with the --proxy-headers parameter and add the X-Forwarded-For and X-Forwarded-Proto header in traefik

Try starting uvicorn with the --proxy-headers parameter and add the X-Forwarded-For and X-Forwarded-Proto header in traefik

Thank you for your help! I'm currently trying to configure traefik but not yet succeed. In the meanwhile, would you mind clarify a little bit how this would affect the above communication steps and realize the aim? I did a quick search for these headers, and it seems they are designed to record the scheme and client ip in between the client and proxy/load-balancer. And run uvicorn with --proxy-headers would ask it to read client ip from X-forwarded-For and use the scheme in X-Forwarded-Proto. So that if client is using https to talk with the traefik proxy, the uvicorn will use https to talk with the client in the end.

But how would that affect the redirect response? Shouldn't it still put a http location there as the request it received from the proxy is http? Feel free to clarify me if I have any misunderstanding.

afaik starlette honors the x-forwareded-proto header and will send redirect responses according to it.

@Reapor-Yurnero

uvicorn handles trailing slash redirects by default, you can disable this by passing redirect_slashes=False to your APIRouter.

Given your proxy is communicating with uvicorn via HTTP, it will naturally redirect to the same scheme (http). As @SebastianLuebke mentioned, uvicorn* does honor the X-FORWARDED-PROTO if enabled. It is enabled by default, and will only populate this information for trusted clients, which are supplied to uvicorn via the command line, or an environment variable:

  --forwarded-allow-ips TEXT      Comma seperated list of IPs to trust with
                                  proxy headers. Defaults to the
                                  $FORWARDED_ALLOW_IPS environment variable if
                                  available, or '127.0.0.1'.

uvicorn internally uses its own ProxyHeadersMiddleware middleware.

The easiest approach is to ensure the X-FORWARDED-PROTO is set to https, and that your proxy's IP is given to uvicorn via CLI or env ($FORWARDED_ALLOW_IPS).

In the case of Traefik, you can enable X-FORWARDED-PROTO behaviour via the sslProxyHeaders middleware. For me, this was as simple as adding the following two docker labels to my API container:

- traefik.http.middlewares.backend.headers.sslProxyHeaders.X-FORWARDED-PROTO=https
- traefik.http.routers.backend.middlewares=backend

One issue I personally had using Docker was passing the proxy IP to uvicorn. Using an environment variable in my docker-compose wasn't ideal, as it'd have to resolve the proxy IP by name dynamically (as uvicorn simply reads this as a string, for string comparison to the client address).

One solution to this would be setting FORWARDED_ALLOW_IPS=*, allowing uvicorn to respect any clients headers, although, perhaps not the most ideal situation, depending on your network setup.

Alternatively, you can add the ProxyHeadersMiddleware yourself via FastAPI. It takes the trusted_hosts in its ctor, meaning you can resolve your proxy IP at runtime, e.g:

from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
...
proxy_ip = socket.gethostbyname("proxy-container-name")
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=proxy_ip)
Was this page helpful?
0 / 5 - 0 ratings