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

Instance Method Details

#enforce_throttlesObject

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

  message = "Rate limit exceeded. Retry in #{result[:retry_after]}s."
  return render_error(message: message, status: :too_many_requests, code: "rate_limited") if respond_to?(:render_error)

  render json: { success: false, error: { message: message, code: "rate_limited" } }, status: :too_many_requests
end