43

I have an API on FastAPI and i need to get the client real IP address when he request my page.

I'm ty to use starlette Request. But it returns my server IP, not client remote IP.

My code:

@app.post('/my-endpoint')
async def my_endpoint(stats: Stats, request: Request):
    ip = request.client.host
    print(ip)
    return {'status': 1, 'message': 'ok'}

What i'm doing wrong? How to get real IP (like in Flask request.remote_addr)?

davidism
  • 121,510
  • 29
  • 395
  • 339
Nataly Firstova
  • 661
  • 1
  • 5
  • 12

11 Answers11

45

request.client should work, unless you're running behind a proxy (e.g. nginx) in that case use uvicorn's --proxy-headers flag to accept these incoming headers and make sure the proxy forwards them.

Hedde van der Heide
  • 21,841
  • 13
  • 71
  • 100
  • 1
    That is correct, but do notice also the comment of @RcoderNY I would like to confirm this answer with my case described here https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/224#issuecomment-1429593840 – Athanassios Feb 14 '23 at 12:13
24

The FastAPI using-request-directly doc page shows this example:

from fastapi import FastAPI, Request

app = FastAPI()


@app.get("/items/{item_id}")
def read_root(item_id: str, request: Request):
    client_host = request.client.host
    return {"client_host": client_host, "item_id": item_id}

Having had this example would have saved me ten minutes of mussing with Starlette's Request class

harrolee
  • 495
  • 5
  • 11
  • 1
    In my case, it only shows my IPv6. But how do I obtain both IPv4 and IPv6? – Houman Jan 29 '23 at 10:52
  • 2
    @Houman, you don't. IP4 and IP6 are totally seperate, and if a connection is made over IPv6, there's no way to know the corresponding IP4-address. In fact, in principle it's possible that the client doesn't even _have_ an IP4-address. To use an old-fashioned analogy, it's like asking how to get the phonenumber for voicecalls from someone, if you got a fax from them. – Emil Bode Feb 24 '23 at 11:43
  • @EmilBode If you open websites like ipleak.net, they show both. – Houman Feb 25 '23 at 12:25
  • 1
    @Houman, not always. In my case, behind a proxy, I only see an IP4-address. The thing to realise, is that services like that can afford to apply complicated tricks where they use multiple connections. But if you only have one (like with FastAPI), there's only one IP-address applicable. To get back to the analogy of a fax, I'm sure it's quite easy to find a telephonenumber of a company of which you have a fax-number. But that doesn't mean fax and telephone are always inextricably linked – Emil Bode Mar 03 '23 at 10:17
8

if you use the nginx and uvicorn,you should set proxy-headers for uvicorn,and your nginx config should be add HostX-Real-IPand X-Forwarded-For.
e.g.

server {
  # the port your site will be served on
    listen 80;
  # the domain name it will serve for
    server_name <your_host_name>; # substitute your machine's IP address or FQDN

#    add_header Access-Control-Allow-Origin *;
    # add_header Access-Control-Allow-Credentials: true;
    add_header Access-Control-Allow-Headers Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE;
    add_header access-control-allow-headers authorization;
    # Finally, send all non-media requests to the Django server.
    location / {
        proxy_pass http://127.0.0.1:8000/; # the uvicorn server address
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}

on the nginx document:

This middleware can be applied to add HTTP proxy support to an
application that was not designed with HTTP proxies in mind. It
sets REMOTE_ADDR, HTTP_HOST from X-Forwarded headers. While
Werkzeug-based applications already can use
:py:func:werkzeug.wsgi.get_host to retrieve the current host even if
behind proxy setups, this middleware can be used for applications which
access the WSGI environment directly。
If you have more than one proxy server in front of your app, set
num_proxies accordingly.
Do not use this middleware in non-proxy setups for security reasons.
The original values of REMOTE_ADDR and HTTP_HOST are stored in
the WSGI environment as werkzeug.proxy_fix.orig_remote_addr and
werkzeug.proxy_fix.orig_http_host
:param app: the WSGI application
:param num_proxies: the number of proxy servers in front of the app.  
keul
  • 7,673
  • 20
  • 45
AllenRen
  • 81
  • 1
  • 3
8

You don't need to set --proxy-headers bc it is enabled by default, but it only trusts IPs from --forwarded-allow-ips which defaults to 127.0.0.1

To be safe, you should only trust proxy headers from the ip of your reverse proxy (instead of trust all with '*'). If it's on the same machine then the defaults should work. Although I noticed from my nginx logs that it was using ip6 to communicate with uvicorn so I had to use --forwarded-allow-ips='[::1]' then I could see the ip addresses in FastAPI. You can also use --forwarded-allow-ips='127.0.0.1,[::1]' to catch both ip4 and ip6 on localhost.

--proxy-headers / --no-proxy-headers - Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port 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 IPs to trust with proxy headers. Defaults to the $FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. A wildcard '*' means always trust.

Ref: https://www.uvicorn.org/settings/#http

RcoderNY
  • 1,204
  • 2
  • 16
  • 23
3

If you have configured your nginx configuration properly based on @AllenRen's answer, Try using --proxy-headers and also --forwarded-allow-ips='*' flags for uvicorn.

Ali AzG
  • 1,861
  • 2
  • 18
  • 28
3

You would use the below code to getting the real-IP address from the client. If you have using reverse proxying and port forwarding

@app.post('/my-endpoint')
async def my_endpoint(stats: Stats, request: Request):
    x = 'x-forwarded-for'.encode('utf-8')
    for header in request.headers.raw:
        if header[0] == x:
            print("Find out the forwarded-for ip address")
            origin_ip, forward_ip = re.split(', ', header[1].decode('utf-8'))
            print(f"origin_ip:\t{origin_ip}")
            print(f"forward_ip:\t{forward_ip}")
    return {'status': 1, 'message': 'ok'}
Klaus Wong
  • 59
  • 2
1

Using the Header dependency should let you access the X-Real-IP header.

from fastapi import FastAPI, Depends, Header

app = FastAPI()

@app.get('/')
def index(real_ip: str = Header(None, alias='X-Real-IP')):
return real_ip

Now if you start the server (in this case on port 8000) and hit it with a request with that X-Real-IP header set you should see it echo back.

http :8000/ X-Real-IP:111.222.333.444

HTTP/1.1 200 OK
content-length: 17 
content-type: application/json
server: uvicorn

"111.222.333.444"

0

I have deployed with docker-compose file and changes are

nginx. conf file

 location / {
  proxy_set_header   Host             $host;
  proxy_set_header   X-Real-IP        $remote_addr;
  proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
  proxy_pass http://localhost:8000;
}

Changes in Dockerfile

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]

Changes in docker-compose.yaml file

version: "3.7"
services:
  app:
    build: ./fastapi
    container_name: ipinfo
    restart: always
    ports:
      - "8000:8000"
    network_mode: host

  nginx:
    build: ./nginx
    container_name: nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    network_mode: host

After these changes got client external IP correctly

Nanda Thota
  • 322
  • 3
  • 10
0

Sharing what has worked for me on an Apache server setup on a stand-alone ubuntu-based web-server instance/droplet (Amazon EC2 / DigitalOcean / Hetzner / SSDnodes). TL;DR : use X_Forwarded_For
I'm assuming you have a domain name registered and are pinning your server to it.

In the code

from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/API/path1")
def path1(X_Forwarded_For: Optional[str] = Header(None)):
    print("X_Forwarded_For:",X_Forwarded_For)
    return { "X_Forwarded_For":X_Forwarded_For }

This gives a null when running in local machine and hitting localhost:port/API/path1 , but in my deployed site it's properly giving my IP address when I hit the API.

In the program launch command

uvicorn launch1:app --port 5010 --host 0.0.0.0 --root-path /site1

main program is in launch1.py . Note the --root-path arg here - that's important if your application is going to deployed not at root level of a URL.
This takes care of url mappings, so in the program code above we didn't need to include it in the @app.get line. Makes the program portable - tomorrow you can move it from /site1 to /site2 path without having to edit the code.

In the server setup

The setting on my web-server:

  • Apache server is setup
  • LetsEncrypt SSL is enabled
  • Edit /etc/apache2/sites-available/[sitename]-le-ssl.conf
  • Add these lines inside <VirtualHost *:443> tag:
    ProxyPreserveHost On

    ProxyPass /site1/ http://127.0.0.1:5010/
    ProxyPassReverse /site1/ http://127.0.0.1:5010/
  • Enable proxy_http and restart Apache
a2enmod proxy_http
systemctl restart apache2

some good guides for server setup:

With this all setup, you can hit your api endpoint on https://[sitename]/site1/API/path1 and should see the same IP address in the response as what you see on https://www.whatismyip.com/ .

Nikhil VJ
  • 5,630
  • 7
  • 34
  • 55
0

I have docker-compose and nginx proxy. The following helped:

  1. in forwarded-allow-ips specified '*' (environment variable in docker-compose.yml file)

- FORWARDED_ALLOW_IPS=*

  1. Added the code to the nginx.conf file as recommended by @allenren
location /api/ {
    proxy_pass http://backend:8000/;
    proxy_set_header   Host             $host;
    proxy_set_header   X-Real-IP        $remote_addr;
    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
}
Maksim
  • 11
  • 2
0

If you are using nginx as a reverse proxy; the direct solution is to include the proxy_params file like so:

location /api {
    include proxy_params;
    proxy_pass http://localhost:8000;
}
Emeka Augustine
  • 891
  • 1
  • 12
  • 17