0

I'd like to know the following

  • How does Starlette set the "url" property in the Request object - especially when operated behind a NGINX proxy.
  • How can request.url be manipulated on the NGINX level -- setting proxy headers doesn't modify it at all?
  • How can request.url be manipulated via a Starlette Middleware -- somehow what I did doesn't have any effect?

The setting is like this:

I have a FastAPI app running behind a NGINX proxy. Via the browser, I send requests to NGINX via HTTPS (https://www.example.com) , but whatever I do, request.url on Starlette's side is always http://www.example.com.

I made a small FastAPI endpoint for demonstration purposes

@app.get("/test")
def some_test(request: Request):
    return {"request.url": request.url,
            "request['headers']": request["headers"],
            "request.client": request.client}

and in the following screenshots I show what I get (domain is anonymized; set to xyz): enter image description here

  1. I call in the browser https://www.example.com/test
  2. In the console, I see that properly the request is issues to https://www.example.com/test
  3. But when looking at Starlette's Request.url, it says http://www.example.com/test

Is this behaviour how it should be? In the screenshot I also print the host and x-forwarded-proto and x-forwarded-schema which I set the NGINX config to HTTPS, but the object Request isn't modified at all by that.

What can I do to correct this on the NGINX or FastAPI/Starlette level?

  • I already tried setting various commands like
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Scheme $scheme;
    
    with no success at all.
  • I tried to write a custom Starlette Middleware (I know that's quite ugly, but I wanted to try this out to better understand, what's going on)
    from fastapi import Request
    from starlette.datastructures import URL
    
    @app.middleware("http")
    async def some_middlware(request: Request, call_next):
    
        if "/test" in str(request.url):
            print(f"before {request.url}")
            request._url = URL(str(request.url).replace("http", "https"))
            print(f"after {request.url}")
    
        response = await call_next(request)
        return response
    
    but this also doesn't modify the request.url shown in the response.
physicus
  • 361
  • 4
  • 12
  • 1
    How are you running Starlette? With `uvicorn`? In that case, uvicorn has `--proxy-headers` to provide request metadata from the expected proxy headers: https://www.uvicorn.org/deployment/#running-from-the-command-line - in particular, see the section [about "Running behind nginx"](https://www.uvicorn.org/deployment/#running-behind-nginx). – MatsLindh Jul 12 '23 at 18:33
  • @MatsLindh Now it works! I already started uvicorn with `--proxy-headers` and --forwarded-allow-ips. But my problem was, that I used the parameter for the allowed IPs incorrectly. This doesn't work: `uvicorn --proxy-headers --forwarded-allow-ips='*'`. This works instead: `uvicorn --proxy-headers --forwarded-allow-ips=*`. (Note the quotation marks) To be precises, as i'm using it in a Dockerfile, my entrypoint looks like `ENTRYPOINT ["uvicorn", "main:app", "--proxy-headers", "--forwarded-allow-ips=*", "--host", "0.0.0.0"]` – physicus Jul 26 '23 at 12:38

1 Answers1

0

Now the proxying of the schema works.

The problem was,the following:

Before my Dockerfile looked like

ENTRYPOINT ["uvicorn", "main:app", "--proxy-headers", "--forwarded-allow-ips='*'", "--host", "0.0.0.0"]

The quotation marks in the --forwarded-allow-ips parameter do not work. Instead the entrypoint should read

ENTRYPOINT ["uvicorn", "main:app", "--proxy-headers", "--forwarded-allow-ips=*", "--host", "0.0.0.0"]

Hint: To be a bit cleaner, one should not allow * as an IP address, instead you should specify the IP address.

physicus
  • 361
  • 4
  • 12