Retrieving a client’s external IP address in a Phoenix application can be helpful for logging, security, and analytics purposes. If your app is behind a reverse proxy (like a load balancer or Nginx), the client IP may not be directly accessible, as the proxy might handle requests on behalf of the client. However, many proxies include the client IP in request headers, which Phoenix can use to reliably fetch the client’s IP address.

In this post, we’ll cover how to retrieve the client’s IP address from the request headers and display it in a Phoenix application.

Why use request headers?

When an application is deployed behind a reverse proxy, the client’s IP address might be masked by the proxy's IP. To address this, reverse proxies typically add headers like X-Forwarded-For, X-Real-IP, or Forwarded that carry the original IP address of the client.

Using these headers allows us to avoid external services to obtain the IP, making the process faster and more secure.

Step 1: retrieve the IP address from request headers

The Phoenix Plug.Conn struct provides functions for accessing request headers. Here’s a simple way to implement a helper function that checks for the client IP in headers, falling back to conn.remote_ip if headers are absent or untrusted.

Let’s create a helper function, get_client_ip/1, that will:

  1. Check the X-Forwarded-For, X-Real-IP, and Forwarded headers for the client IP in that order.
  2. If none of these headers are found, fall back to conn.remote_ip, which contains the IP from the immediate client (usually the reverse proxy if you’re behind one).
  3. Parse and return the first valid IP address found.

Here’s how to write it:

defmodule MyAppWeb.PageController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    client_ip = get_client_ip(conn)
    render(conn, "index.html", client_ip: client_ip)
  end

  defp get_client_ip(conn) do
    # Check the most common headers for client IP in order of priority
    conn
    |> get_forwarded_ip()
    |> case do
      nil -> conn.remote_ip |> :inet_parse.ntoa() |> to_string()
      ip -> ip
    end
  end

  defp get_forwarded_ip(conn) do
    # Try headers typically set by proxies
    forwarded_for = List.first(get_req_header(conn, "x-forwarded-for"))
    real_ip = List.first(get_req_header(conn, "x-real-ip"))
    forwarded = List.first(get_req_header(conn, "forwarded"))

    cond do
      forwarded_for -> String.split(forwarded_for, ",") |> List.first() |> String.trim()
      real_ip -> real_ip
      forwarded && Regex.run(~r/for=(?<ip>[^\s;]+)/, forwarded, capture: :all_but_first) -> List.first(Regex.run(~r/for=(?<ip>[^\s;]+)/, forwarded))
      true -> nil
    end
  end
end

This code performs the following:

  • The get_forwarded_ip/1 function extracts the first IP address it finds in the headers X-Forwarded-For, X-Real-IP, or Forwarded.
  • If no IP is found in the headers, get_client_ip/1 falls back to conn.remote_ip, which retrieves the IP of the last immediate client in the request chain.

Step 2: display the IP address in the view

Now that we have the client’s IP address, let’s display it in the view. In index.html.eex, you can add a simple display for the IP address:

<%= if @client_ip do %>
  <p>Your IP address is: <%= @client_ip %></p>
<% else %>
  <p>Could not retrieve your IP address.</p>
<% end %>

Step 3: testing and verification

Local testing

If you’re testing locally, you might not see headers like X-Forwarded-For, especially if you’re not behind a proxy. In this case, conn.remote_ip will return your local IP (e.g., 127.0.0.1), which is expected behavior.

Production setup

In production, ensure that your reverse proxy (such as Nginx, AWS ELB, or Heroku’s router) is configured to forward the client IP through headers. This setup typically involves enabling the X-Forwarded-For or X-Real-IP header.

For example, with Nginx, you can configure it as follows:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;

Security considerations

Headers like X-Forwarded-For can be spoofed, as clients can send arbitrary values. For security purposes, ensure that these headers are only trusted from requests that come through your reverse proxy. Many reverse proxies allow you to configure this behavior, restricting which IP addresses or ranges can set these headers.

Trying it out

If you want to try it out, you can use this test page.

Conclusion

Retrieving the client’s IP address in a Phoenix app is straightforward once you know how to handle the relevant headers. By examining X-Forwarded-For, X-Real-IP, and Forwarded headers in order, and using conn.remote_ip as a fallback, you can reliably capture the client IP without needing an external service.

Using this approach can enhance your logging, security monitoring, and analytics without adding extra dependencies or latency.