Module: ConcernsOnRails::Controllers::Throttleable
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/concerns_on_rails/controllers/throttleable.rb
Overview
Per-request rate limiting with a store-agnostic, injectable backend. When a rule’s limit is exceeded the request is halted with 429 plus ‘Retry-After` and `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers.
class Api::BaseController < ApplicationController
include ConcernsOnRails::Controllers::Throttleable
self.throttleable_store = Rails.cache # must support atomic #increment
throttle_by limit: 100, period: 1.minute # by IP (default)
throttle_by limit: 5, period: 1.minute, only: :create,
by: -> { current_user&.id || request.remote_ip }
end
Fixed-window counter: the key embeds a floored time bucket (‘epoch / period`) so each window starts clean and `X-RateLimit-Reset` is exact. The store MUST support atomic increment-with-expiry (`Rails.cache` with `#increment`, or Redis); a non-atomic store under-counts under concurrency. There is no in-process default store on purpose — configure one explicitly or the first throttled request raises ArgumentError.
Defined Under Namespace
Modules: ClassMethods
Constant Summary collapse
- DEFAULT_DISCRIMINATOR =
Default discriminator — one counter per client IP. Evaluated with instance_exec on the controller, so ‘request` resolves normally.
-> { request.remote_ip }
Instance Method Summary collapse
-
#enforce_throttles ⇒ Object
Public so subclasses can override.
-
#throttled_response(_rule, result) ⇒ Object
Public override point for the 429 body.
Instance Method Details
#enforce_throttles ⇒ Object
Public so subclasses can override. Applies each in-scope rule; the first rule that exceeds its limit halts the request with a 429.
79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/concerns_on_rails/controllers/throttleable.rb', line 79 def enforce_throttles self.class.throttleable_rules.each do |rule| next unless throttle_rule_applies?(rule) result = register_throttle_hit(rule) emit_throttle_headers(rule, result) return throttled_response(rule, result) if result[:count] > rule[:limit] end nil end |
#throttled_response(_rule, result) ⇒ Object
Public override point for the 429 body.
92 93 94 95 96 97 98 99 |
# File 'lib/concerns_on_rails/controllers/throttleable.rb', line 92 def throttled_response(_rule, result) return unless respond_to?(:response) && response = "Rate limit exceeded. Retry in #{result[:retry_after]}s." return render_error(message: , status: :too_many_requests, code: "rate_limited") if respond_to?(:render_error) render json: { success: false, error: { message: , code: "rate_limited" } }, status: :too_many_requests end |