Search code examples
azurefastapiuvicornstarlette

Why does my FastAPI application redirect to HTTP and not HTTPS?


I'm using FastAPI, running via Uvicorn, with the below (default) config:

app = FastAPI(..., redirect_slashes=True)

This should reroute e.g. https://example.com/test/ to https://example.com/test.

When running locally via HTTPS, FastAPI (Starlette / Uvicorn) redirect routes successfully from HTTPS to HTTPS, and from HTTP to HTTP.

When I run the application on Azure App Service via HTTPs, the Location header returned in the 307 Temporary Redirect response uses HTTP. For example, instead of redirecting https://example.com/test/ to https://example.com/test, I am redirected incorrectly to http://example.com/test.

This breaks my application as it's not running on HTTP with the browser also blocking the request:

Mixed Content: The page at '' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint ''. This request has been blocked; the content must be served over HTTPS.

Why is the redirect working locally but not when I have deployed the application?


Solution

  • TLDR: use uvicorn ... --forwarded-allow-ips * or uvicorn.run('xxx', forwarded_allow_ips='*')


    By convention, proxies transmit information within HTTP headers.

    As documented by Microsoft,

    Application gateway inserts six additional headers to all requests before it forwards the requests to the backend. These headers are x-forwarded-for, x-forwarded-port, x-forwarded-proto, x-original-host, x-original-url, and x-appgw-trace-id. The format for x-forwarded-for header is a comma-separated list of IP:port.

    Out of these headers, the X-Forwarded-Proto header specifies the originating scheme(s) - HTTP or HTTPS. Uvicorn docs state that it is this header that is utilised to determine & set the ASGI scheme.

    Starlette's route redirection code specifically creates an instance of the URL class that uses the ASGI's scheme. Therefore, making sure this header is set correctly by Uvicorn ensures that the URL scheme used for redirection is correct.


    By default, Uvicorn's --proxy-headers setting is enabled. This means it will leverage the x-forwarded-proto header alongside the x-forwarded-for header to gather remote address information. This default behaviour also applies when running Uvicorn programmatically using uvicorn.run.

    However, a crucial point is highlighted:

    --proxy-headers / --no-proxy-headers - Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate remote address info. Defaults to enabled, but is restricted to only trusting connecting IPs in the forwarded-allow-ips configuration.

    --forwarded-allow-ips Comma separated list of IP Addresses, IP Networks, or literals (e.g. UNIX Socket path) to trust with proxy headers. Defaults to the $FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. The literal '*' means trust everything.

    Examining the code for ProxyHeadersMiddleware confirms the above:

    def __init__(self, app: ASGI3Application, trusted_hosts: list[str] | str = "127.0.0.1") -> None:
        self.app = app
        self.trusted_hosts = _TrustedHosts(trusted_hosts)
    
    ...
    
    if client_host in self.trusted_hosts:
        headers = dict(scope["headers"])
    
        if b"x-forwarded-proto" in headers:
            x_forwarded_proto = headers[b"x-forwarded-proto"].decode("latin1").strip()
    
            if x_forwarded_proto in {"http", "https", "ws", "wss"}:
                if scope["type"] == "websocket":
                    scope["scheme"] = x_forwarded_proto.replace("http", "ws")
                else:
                    scope["scheme"] = x_forwarded_proto
    

    This explains why it works locally, as 127.0.0.1 is the default trusted host when not specified.

    To address this, when using Uvicorn via the CLI, you can provide the --forwarded-allow-ips * flag and value. Alternatively, when using it programmatically, specify forwarded_allow_ips='*' (e.g. uvicorn.run('xxx', forwarded_allow_ips='*')). You can also set the FORWARDED_ALLOW_IPS environment variable.

    Set the value in any of these cases to a wildcard if you fully trust the proxy and route your traffic only through the proxy. Otherwise, clients with direct access can potentially spoof the 2 headers.


    Ensuring the X-Forwarded-Proto header is set appropriately by Uvicorn is crucial for accurate URL scheme determination during route redirection in Starlette applications (which FastAPI uses).

    While Uvicorn's default behaviour enables usage of this header on localhost, configuring trusted proxies via --forwarded-allow-ips allows usage in production environments while preventing potential spoofing by unauthorised clients.