Rate Limits Phoenix
Tags: elixir, phoenix, ratelimits, web
Published: 2022-09-22 15:31:48

Ever wondered how to protect your APi/Backend from Spam? Here is how. 

Requirements: 
- Elixir v13.3.2 + 
- Phoenix v1.6.10+
- Basic Knowledge of Elixir

```
$ elixir -v
Elixir 1.13.2 (compiled with Erlang/OTP 24)

$ mix phx.new --version
Phoenix installer v1.6.10
```

## Getting started

> Create a new Phoenix Project
```
$ mix phx.new ratelimit --no-mailer --no-assets --no-html --no-ecto --no-dashboard
```
If you will be asked to install some dependencies, answer with `y` (yes).

> Adding Dependencies

Lets add some dependencies that will help us doing all of this
`mix.exs`
```elixir
defp deps do
    [
      # here is some other stuff

      # <-- add the stuff below here -->

      # http
      {:httpoison, "~> 1.8"},
      # rate limit
      {:hammer, "~> 6.1"},
    ]
  end
``` 

For the rate limits, we will use the following library
https://github.com/ExHammer/hammer

> Install dependencies 

```
$ mix deps.get
```

> Project files 

Now lets get our hands dirty by creating some helper functions.

`lib/ratelimit/base.ex`
```elixir
defmodule Ratelimit.Base do
  use HTTPoison.Base

  @moduledoc """
  This handles HTTP requests without api key (basic requests).
  """

  def process_request_headers(headers) do
    [{"Content-Type", "application/json"} | headers]
  end
end
```

Now lets add a function that gets the IP of the user visiting our website. 
`lib/ratelimit/helper/getip.ex`

```elixir
defmodule Ratelimit.IP do
  @doc """
  Get the IP address of the current user visiting the route.
  Formatted as a string: "123.456.78.9"
  """
  # {:ok, String.t()} | {:error, :api_down}
  @spec getIP() :: {String.t() | :api_down}
  def getIP() do
    ip_url = "https://api.ipify.org/"

    case Ratelimit.Base.get!(ip_url) do
      %HTTPoison.Response{body: body, status_code: 200} ->
        body

      %HTTPoison.Response{status_code: status_code} when status_code > 399 ->
        IO.inspect(status_code, label: "STATUS_CODE")
        :error

      _ ->
        raise "APi down"
    end
  end
end
```
Awesome, now lets create a file that handles our ratelimits
`lib/ratelimit/util/ratelimit.ex`

```elixir
defmodule RatelimitWeb.Plugs.RateLimiter do
  import Plug.Conn
  use RatelimitWeb, :controller

  alias Ratelimit.IP
  require Logger

  # two request / minute are allowed
  @limit 2

  def init(options), do: options

  def call(conn, _opts) do
    # call the ip function
    ip = IP.getIP()

    case Hammer.check_rate(ip, 60_000, @limit) do
      {:allow, count} ->
        assign(conn, :requests_count, count)

      {:deny, _limit} ->
        # Beep Boop, remove this in production
        Logger.debug("Rate limit exceeded for #{inspect(ip)}")
        error_response(conn)
    end
  end

  defp error_response(conn) do
    conn
    |> put_status(:service_unavailable) # set the json status
    |> json(%{message: "Please wait before sending another request."}) # return an error message
    |> halt() # stop the process
  end
end
```

Now we have to configure Hammer in our config files.
For that, open `config/config.exs` and add this line here:
```elixir
# Config the rate limiter
config :hammer,
  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
```

> Adding the Controller 

Now we have to create a simple controller for our website.
`lib/ratelimit_web/controllers/page_controller.ex`
```elixir
defmodule RatelimitWeb.PageController do
  use RatelimitWeb, :controller

  def index(conn, _params) do
    send_resp(conn, 200, "Hello there!")
  end
end
```

and we have to edit our `lib/ratelimit_web/router.ex` to the following
```elixir
pipeline :api do
    # add the rate limit plug here
    plug RatelimitWeb.Plugs.RateLimiter
    plug :accepts, ["json"]
  end

scope "/api", RatelimitWeb do
    pipe_through :api

    # add this here
    get "/test", PageController, :index
  end
```

> Start the Server 

Now lets try to start our APi with the following
```elixir
$ mix phx.server
```

After that, navigate to the following URL: 
`http://localhost:4000/api/test`.
You should see the following: 

<p align="center">
<img src="https://media.discordapp.net/attachments/893792219390156850/1022528040359690300/unknown.png"  alt="showcase" width="200" height="200"/></a>
<p>

> Sending requests

Now to test our rate limits, send multiple request to the same URL, by just refreshing the page more than `2` times. 

You will see something changing suddenly, like this: 
<p align="center">
<img src="https://media.discordapp.net/attachments/893792219390156850/1022528110975000717/unknown.png" width="200" height="200"/></a>
<p>
🎉🎉🎉 You are awesome!

### Additional Things

If you want to change the message, you can easily do this in the `lib/ratelimit/util/ratelimit.ex` file. 

In production, remove the IP inspect at the `Logger.debug`.
 --- 
The code can be found here: https://github.com/vKxni/ratelimit