DEV Community

vKxni
vKxni

Posted on

Rate Limits Phoenix

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
Enter fullscreen mode Exit fullscreen mode

Getting started

Create a new Phoenix Project

$ mix phx.new ratelimit --no-mailer --no-assets --no-html --no-ecto --no-dashboard
Enter fullscreen mode Exit fullscreen mode

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

defp deps do
    [
      # here is some other stuff

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

      # http
      {:httpoison, "~> 1.8"},
      # rate limit
      {:hammer, "~> 6.1"},
    ]
  end
Enter fullscreen mode Exit fullscreen mode

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

Install dependencies

$ mix deps.get
Enter fullscreen mode Exit fullscreen mode

Project files

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

lib/ratelimit/base.ex

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

Awesome, now lets create a file that handles our ratelimits
lib/ratelimit/util/ratelimit.ex

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
Enter fullscreen mode Exit fullscreen mode

Now we have to configure Hammer in our config files.
For that, open config/config.exs and add this line here:

# Config the rate limiter
config :hammer,
  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
Enter fullscreen mode Exit fullscreen mode

Adding the Controller

Now we have to create a simple controller for our website.
lib/ratelimit_web/controllers/page_controller.ex

defmodule RatelimitWeb.PageController do
  use RatelimitWeb, :controller

  def index(conn, _params) do
    send_resp(conn, 200, "Hello there!")
  end
end
Enter fullscreen mode Exit fullscreen mode

and we have to edit our lib/ratelimit_web/router.ex to the following

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
Enter fullscreen mode Exit fullscreen mode

Start the Server

Now lets try to start our APi with the following

$ mix phx.server
Enter fullscreen mode Exit fullscreen mode

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


showcase

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:




πŸŽ‰πŸŽ‰πŸŽ‰ 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

Top comments (0)

Some comments have been hidden by the post's author - find out more