README

AshRateLimiter
Welcome! This is an extension for the Ash framework which protects actions from abuse by enforcing rate limits.
Uses the excellent hammer to provide rate limiting functionality.
Installation
Add ash_rate_limiter to your list of dependencies in mix.exs:
def deps do
[
{:ash_rate_limiter, "~> 0.1.1"}
]
endQuick Start
- Configure Hammer: First, you need to configure a Hammer backend. For development, you can use the ETS backend:
# config/config.exs
config :hammer,
backend: {Hammer.Backend.ETS, []}For production, consider using Redis:
# config/config.exs
config :hammer,
backend: {Hammer.Backend.Redis, [
host: "localhost",
port: 6379
]}- Add to your resource: Use the
rate_limitDSL section in your Ash resource:
defmodule MyApp.Post do
use Ash.Resource,
domain: MyApp,
extensions: [AshRateLimiter]
rate_limit do
# Configure hammer backend
hammer Hammer
# Limit create action to 10 requests per 5 minutes
action :create, limit: 10, per: :timer.minutes(5)
# Limit read action to 100 requests per minute
action :read, limit: 100, per: :timer.minutes(1)
end
# ... rest of your resource definition
end- That's it! Your actions are now rate limited. When the limit is exceeded, an
AshRateLimiter.LimitExceedederror will be raised.
Basic Usage
Simple Rate Limiting
rate_limit do
hammer Hammer
action :create, limit: 5, per: :timer.minutes(1)
endPer-User Rate Limiting
rate_limit do
hammer Hammer
action :create,
limit: 10,
per: :timer.minutes(5),
key: fn changeset, context ->
"user:#{context.actor.id}:create"
end
endMultiple Actions
rate_limit do
hammer Hammer
action :create, limit: 10, per: :timer.minutes(5)
action :update, limit: 20, per: :timer.minutes(5)
action :read, limit: 100, per: :timer.minutes(1)
endAdvanced Usage
Manual Integration
For more control, you can add rate limiting directly to specific actions:
defmodule MyApp.Post do
use Ash.Resource, domain: MyApp
actions do
create :create do
change {AshRateLimiter.Change, limit: 10, per: :timer.minutes(5)}
end
read :read do
prepare {AshRateLimiter.Preparation, limit: 100, per: :timer.minutes(1)}
end
end
endOr use the built-in helpers:
actions do
create :create do
change rate_limit(limit: 10, per: :timer.minutes(5))
end
endCustom Key Functions
The key function determines how requests are grouped for rate limiting:
# Rate limit per IP address
key: fn _changeset, context ->
"ip:#{context[:ip_address]}"
end
# Rate limit per user and action
key: fn changeset, context ->
"user:#{context.actor.id}:action:#{changeset.action.name}"
end
# Use the built-in key function with options
key: {&AshRateLimiter.key_for_action/2, include_actor_attributes: [:role]}Error Handling
When rate limits are exceeded, an AshRateLimiter.LimitExceeded exception is raised:
case MyApp.create_post(attrs) do
{:ok, post} ->
# Success
{:ok, post}
{:error, %AshRateLimiter.LimitExceeded{} = error} ->
# Rate limit exceeded
{:error, "Too many requests, please try again later"}
{:error, other_error} ->
# Handle other errors
{:error, other_error}
endIn web applications, the exception includes Plug.Exception behaviour for automatic HTTP 429 responses.
Testing
In test environments, you may want to disable rate limiting or use a test-friendly configuration:
# config/test.exs
config :hammer,
backend: {Hammer.Backend.ETS, []}
# Or disable rate limiting entirely in tests by using a mock