Class: Rack::Ratelimit
- Inherits:
-
Object
- Object
- Rack::Ratelimit
- Defined in:
- lib/rack/ratelimit.rb
Overview
Ratelimit
-
Run multiple rate limiters in a single app
-
Scope each rate limit to certain requests: API, files, GET vs POST, etc.
-
Apply each rate limit by request characteristics: IP, subdomain, OAuth2 token, etc.
-
Flexible time window to limit burst traffic vs hourly or daily traffic:
100 requests per 10 sec, 500 req/minute, 10000 req/hour, etc.
-
Fast, low-overhead implementation using counters per time window:
timeslice = window * ceiling(current time / window) store.incr(timeslice)
Defined Under Namespace
Classes: MemcachedCounter, RedisCounter
Instance Method Summary collapse
-
#apply_rate_limit?(env) ⇒ Boolean
Apply the rate limiter if none of the exceptions apply and all the conditions are met.
-
#call(env) ⇒ Object
Handle a Rack request: * Check whether the rate limit applies to the request.
-
#classify(env) ⇒ Object
Give subclasses an opportunity to specialize classification.
-
#condition(predicate = nil, &block) ⇒ Object
Add a condition that must be met before applying the rate limit.
-
#exception(predicate = nil, &block) ⇒ Object
Add an exception that excludes requests from the rate limit.
-
#initialize(app, options, &classifier) ⇒ Ratelimit
constructor
Takes a block that classifies requests for rate limiting.
Constructor Details
#initialize(app, options, &classifier) ⇒ Ratelimit
Takes a block that classifies requests for rate limiting. Given a Rack env, return a string such as IP address, API token, etc. If the block returns nil, the request won’t be rate-limited. If a block is not given, all requests get the same limits.
Required configuration:
rate: an array of [max requests, period in seconds]: [500, 5.minutes]
and one of
cache: a Dalli::Client instance
redis: a Redis instance
counter: Your own custom counter. Must respond to
`#increment(classification_string, end_of_time_window_timestamp)`
and return the counter value after increment.
Optional configuration:
name: name of the rate limiter. Defaults to 'HTTP'. Used in messages.
status: HTTP response code. Defaults to 429.
conditions: array of procs that take a rack env, all of which must
return true to rate-limit the request.
exceptions: array of procs that take a rack env, any of which may
return true to exclude the request from rate limiting.
logger: responds to #info(message). If provided, the rate limiter
logs the first request that hits the rate limit, but none of the
subsequently blocked requests.
error_message: the message returned in the response body when the rate
limit is exceeded. Defaults to "<name> rate limit exceeded. Please
wait <period> seconds then retry your request."
Example:
Rate-limit bursts of POST/PUT/DELETE by IP address, return 503:
use(Rack::Ratelimit, name: 'POST',
exceptions: ->(env) { env['REQUEST_METHOD'] == 'GET' },
rate: [50, 10.seconds],
status: 503,
cache: Dalli::Client.new,
logger: Rails.logger) { |env| Rack::Request.new(env).ip }
Rate-limit API traffic by user (set by Rack::Auth::Basic):
use(Rack::Ratelimit, name: 'API',
conditions: ->(env) { env['REMOTE_USER'] },
rate: [1000, 1.hour],
redis: Redis.new(ratelimit_redis_config),
logger: Rails.logger) { |env| env['REMOTE_USER'] }
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/rack/ratelimit.rb', line 60 def initialize(app, , &classifier) @app, @classifier = app, classifier @classifier ||= lambda { |env| :request } @name = .fetch(:name, 'HTTP') @max, @period = .fetch(:rate) @status = .fetch(:status, 429) @counter = if counter = [:counter] raise ArgumentError, 'Counter must respond to #increment' unless counter.respond_to?(:increment) counter elsif cache = [:cache] MemcachedCounter.new(cache, @name, @period) elsif redis = [:redis] RedisCounter.new(redis, @name, @period) else raise ArgumentError, ':cache, :redis, or :counter is required' end @logger = [:logger] @error_message = .fetch(:error_message, "#{@name} rate limit exceeded. Please wait #{@period} seconds then retry your request.") @conditions = Array([:conditions]) @exceptions = Array([:exceptions]) end |
Instance Method Details
#apply_rate_limit?(env) ⇒ Boolean
Apply the rate limiter if none of the exceptions apply and all the conditions are met.
105 106 107 |
# File 'lib/rack/ratelimit.rb', line 105 def apply_rate_limit?(env) @exceptions.none? { |e| e.call(env) } && @conditions.all? { |c| c.call(env) } end |
#call(env) ⇒ Object
Handle a Rack request:
* Check whether the rate limit applies to the request.
* Classify the request by IP, API token, etc.
* Calculate the end of the current time window.
* Increment the counter for this classification and time window.
* If count exceeds limit, return a 429 response.
* If it's the first request that exceeds the limit, log it.
* If the count doesn't exceed the limit, pass through the request.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/rack/ratelimit.rb', line 122 def call(env) if apply_rate_limit?(env) && classification = classify(env) # Marks the end of the current rate-limiting window. = @period * (Time.now.to_f / @period).ceil time = Time.at().utc.xmlschema # Increment the request counter. count = @counter.increment(classification, ) remaining = @max - count json = %({"name":"#{@name}","period":#{@period},"limit":#{@max},"remaining":#{remaining < 0 ? 0 : remaining},"until":"#{time}"}) # If exceeded, return a 429 Rate Limit Exceeded response. if remaining < 0 # Only log the first hit that exceeds the limit. if @logger && remaining == -1 @logger.info '%s: %s exceeded %d request limit for %s' % [@name, classification, @max, time] end [ @status, { 'X-Ratelimit' => json, 'Retry-After' => @period.to_s }, [@error_message] ] # Otherwise, pass through then add some informational headers. else @app.call(env).tap do |status, headers, body| headers['X-Ratelimit'] = [headers['X-Ratelimit'], json].compact.join("\n") end end else @app.call(env) end end |
#classify(env) ⇒ Object
Give subclasses an opportunity to specialize classification.
110 111 112 |
# File 'lib/rack/ratelimit.rb', line 110 def classify(env) @classifier.call env end |
#condition(predicate = nil, &block) ⇒ Object
Add a condition that must be met before applying the rate limit. Pass a block or a proc argument that takes a Rack env and returns true if the request should be limited.
90 91 92 93 |
# File 'lib/rack/ratelimit.rb', line 90 def condition(predicate = nil, &block) @conditions << predicate if predicate @conditions << block if block_given? end |
#exception(predicate = nil, &block) ⇒ Object
Add an exception that excludes requests from the rate limit. Pass a block or a proc argument that takes a Rack env and returns true if the request should be excluded from rate limiting.
98 99 100 101 |
# File 'lib/rack/ratelimit.rb', line 98 def exception(predicate = nil, &block) @exceptions << predicate if predicate @exceptions << block if block_given? end |