Packages
A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.
Current section
Files
Jump to
Current section
Files
lib/syntropy_web/plugs/rate_limiter.ex
defmodule SyntropyWeb.Plugs.RateLimiter do
@moduledoc """
Applies `Syntropy.RateLimiter` to API pipelines.
Each pipeline declares its class (`plug SyntropyWeb.Plugs.RateLimiter,
class: :read`). Requests are keyed by the bearer token (hashed — the
plaintext never becomes a bucket key) or, absent one, by the peer IP.
Exhausted buckets answer 429 with a `retry-after` header.
"""
import Plug.Conn
import Phoenix.Controller, only: [json: 2]
alias Syntropy.RateLimiter
alias SyntropyWeb.ApiAuth
@spec init(keyword()) :: RateLimiter.class()
def init(opts), do: Keyword.get(opts, :class, :read)
@spec call(Plug.Conn.t(), RateLimiter.class()) :: Plug.Conn.t()
def call(conn, class) do
if RateLimiter.enabled?() do
case RateLimiter.allow(identity(conn), class) do
:ok ->
conn
{:error, retry_after_seconds} ->
conn
|> put_resp_header("retry-after", Integer.to_string(retry_after_seconds))
|> put_status(:too_many_requests)
|> json(%{
error: %{
code: "rate_limited",
message:
"Rate limit exceeded for #{class} requests. Retry in #{retry_after_seconds}s."
}
})
|> halt()
end
else
conn
end
end
defp identity(conn) do
case get_req_header(conn, "authorization") do
[header | _rest] when is_binary(header) and header != "" ->
"token:" <> ApiAuth.hash_token(header)
_missing ->
"ip:" <> ip_string(conn.remote_ip)
end
end
defp ip_string(ip) do
ip
|> :inet.ntoa()
|> to_string()
end
end