rails_rate_limit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +74 -23
- data/lib/rails_rate_limit/configuration.rb +13 -12
- data/lib/rails_rate_limit/controller.rb +2 -2
- data/lib/rails_rate_limit/klass.rb +40 -3
- data/lib/rails_rate_limit/rate_limiter.rb +0 -2
- data/lib/rails_rate_limit/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f84a9dd27c84c9d0519657dcdc6a03da54339a00951fba0e4dfe847e210bc416
|
4
|
+
data.tar.gz: 68ed631f37784ac86f12e261cd28e99caf879f4b604604f5c0717b9e9a071062
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d5822c1423345099c7d42a21ca11951720de2f9be00e7858fdf9ffee69b3e970454ed878e57e36c75ee74e49c213a0de1a079f2a476c52c95cdaabefdc34138
|
7
|
+
data.tar.gz: 736ce16dddb1b5e238e88570cd67cbce3895f2bb51a61a10a5d635112d4fe3d3489a33bf01735ddc67ebf350a669f668c3ce1952324a8ba5145649c0806cc379
|
data/README.md
CHANGED
@@ -1,14 +1,13 @@
|
|
1
|
-
# Rails Rate Limit
|
1
|
+
# **Rails Rate Limit**
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/rails_rate_limit)
|
4
4
|
[](https://github.com/kasvit/rails_rate_limit/actions)
|
5
5
|
|
6
|
-
A flexible and robust rate limiting solution for Ruby on Rails applications. The gem implements a sliding window log algorithm, which means it tracks the exact timestamp of each request and calculates the count within a sliding time window. This provides more accurate rate limiting compared to fixed window approaches.
|
6
|
+
A flexible and robust rate limiting solution for Ruby on Rails applications. The gem implements a **sliding window log** algorithm, which means it tracks the exact timestamp of each request and calculates the count within a sliding time window. This provides more accurate rate limiting compared to fixed window approaches.
|
7
7
|
|
8
8
|
For example, if you set a limit of 100 requests per hour, and a user makes 100 requests at 2:30 PM, they won't be able to make another request until some of those requests "expire" after 2:30 PM the next hour. This prevents the common issue with fixed windows where users could potentially make 200 requests around the window boundary.
|
9
9
|
|
10
|
-
|
11
|
-
Inspired by https://github.com/rails/rails/blob/8-0-sec/actionpack/lib/action_controller/metal/rate_limiting.rb.
|
10
|
+
The gem supports rate limiting for both HTTP requests (in controllers) and instance method calls (in any Ruby class), with multiple storage backends (Redis, Memcached, Memory).
|
12
11
|
|
13
12
|
## Installation
|
14
13
|
|
@@ -30,7 +29,7 @@ Create an initializer `config/initializers/rails_rate_limit.rb`:
|
|
30
29
|
|
31
30
|
```ruby
|
32
31
|
RailsRateLimit.configure do |config|
|
33
|
-
#
|
32
|
+
# Optional: Choose your storage backend (default: :memory)
|
34
33
|
config.default_store = :redis # Available options: :redis, :memcached, :memory
|
35
34
|
|
36
35
|
# Optional: Configure Redis connection (required if using Redis store)
|
@@ -47,10 +46,12 @@ RailsRateLimit.configure do |config|
|
|
47
46
|
)
|
48
47
|
|
49
48
|
# Optional: Configure logging
|
49
|
+
# set `nil` to disable logging
|
50
50
|
config.logger = Rails.logger
|
51
51
|
|
52
52
|
# Optional: Configure default handler for controllers (HTTP requests)
|
53
|
-
config.
|
53
|
+
config.handle_controller_exceeded = -> {
|
54
|
+
# Default handler returns a JSON response with a 429 status code
|
54
55
|
render json: {
|
55
56
|
error: "Too many requests",
|
56
57
|
retry_after: response.headers["Retry-After"]
|
@@ -58,10 +59,9 @@ RailsRateLimit.configure do |config|
|
|
58
59
|
}
|
59
60
|
|
60
61
|
# Optional: Configure default handler for methods
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
Rails.logger.warn("Rate limit exceeded for #{self.class.name}")
|
62
|
+
# By default, it raises RailsRateLimit::RateLimitExceeded
|
63
|
+
config.handle_klass_exceeded = -> {
|
64
|
+
raise RailsRateLimit::RateLimitExceeded, "Rate limit exceeded"
|
65
65
|
}
|
66
66
|
end
|
67
67
|
```
|
@@ -99,33 +99,65 @@ end
|
|
99
99
|
|
100
100
|
### Rate Limiting Methods
|
101
101
|
|
102
|
-
You can limit
|
102
|
+
You can limit both instance and class methods in your classes:
|
103
103
|
|
104
104
|
```ruby
|
105
105
|
class ApiClient
|
106
|
-
include RailsRateLimit::Klass
|
106
|
+
include RailsRateLimit::Klass
|
107
107
|
|
108
|
+
# Instance method
|
108
109
|
def make_request
|
109
110
|
# Your API call logic here
|
110
111
|
end
|
111
112
|
|
112
|
-
#
|
113
|
-
|
113
|
+
# Class method
|
114
|
+
def self.bulk_request
|
115
|
+
# Your API call logic here
|
116
|
+
end
|
117
|
+
|
118
|
+
# Rate limit for instance method
|
114
119
|
set_rate_limit :make_request,
|
115
120
|
limit: 100,
|
116
121
|
period: 1.minute
|
117
122
|
|
118
|
-
#
|
123
|
+
# Rate limit for class method
|
124
|
+
set_rate_limit :bulk_request,
|
125
|
+
limit: 10,
|
126
|
+
period: 1.hour
|
127
|
+
|
128
|
+
# Advanced usage with all options (instance method)
|
119
129
|
set_rate_limit :another_method,
|
120
130
|
limit: 10, # Maximum calls allowed
|
121
131
|
period: 1.hour, # Time window for the limit
|
122
132
|
by: -> { "client:#{id}" }, # Method call identifier
|
123
133
|
store: :memcached, # Override default store
|
124
134
|
on_exceeded: -> { # Custom error handler
|
135
|
+
# You can handle the error here and return any value (including nil)
|
125
136
|
notify_admin
|
126
137
|
log_exceeded_event
|
127
|
-
|
138
|
+
nil # Method will return nil
|
139
|
+
}
|
140
|
+
|
141
|
+
# Advanced usage with all options (class method)
|
142
|
+
set_rate_limit :another_class_method,
|
143
|
+
limit: 5, # Maximum calls allowed
|
144
|
+
period: 1.day, # Time window for the limit
|
145
|
+
by: -> { "global:#{name}" }, # Method call identifier
|
146
|
+
store: :redis, # Override default store
|
147
|
+
on_exceeded: -> { # Custom error handler
|
148
|
+
log_exceeded_event
|
149
|
+
"Rate limit exceeded" # Return custom message
|
128
150
|
}
|
151
|
+
|
152
|
+
# Direct rate limit setting
|
153
|
+
# You can also set rate limits directly if you have both instance and class methods with the same name
|
154
|
+
set_instance_rate_limit :process, # For instance method
|
155
|
+
limit: 10,
|
156
|
+
period: 1.hour
|
157
|
+
|
158
|
+
set_class_rate_limit :process, # For class method
|
159
|
+
limit: 5,
|
160
|
+
period: 1.hour
|
129
161
|
end
|
130
162
|
```
|
131
163
|
|
@@ -135,8 +167,9 @@ For both controllers and methods:
|
|
135
167
|
- `limit`: (Required) Maximum number of requests/calls allowed
|
136
168
|
- `period`: (Required) Time period for the limit (in seconds or ActiveSupport::Duration)
|
137
169
|
- `by`: (Optional) Lambda/Proc to generate unique identifier
|
138
|
-
- Default for controllers: `request.remote_ip`
|
139
|
-
- Default for methods: `"#{
|
170
|
+
- Default for controllers: `"#{controller.class.name}:#{controller.request.remote_ip}"`
|
171
|
+
- Default for instance methods: `"#{self.class.name}##{method_name}:#{respond_to?(:id) ? 'id='+id.to_s : 'object_id='+object_id.to_s}"`
|
172
|
+
- Default for class methods: `"#{class.name}.#{method_name}"`
|
140
173
|
- `store`: (Optional) Override default storage backend (`:redis`, `:memcached`, `:memory`)
|
141
174
|
- `on_exceeded`: (Optional) Custom handler for rate limit exceeded
|
142
175
|
|
@@ -149,16 +182,34 @@ Additional options for controllers:
|
|
149
182
|
The gem provides different default behaviors for controllers and methods:
|
150
183
|
|
151
184
|
1. For controllers (HTTP requests):
|
152
|
-
- The `on_exceeded` handler (or
|
185
|
+
- The `on_exceeded` handler (or default handler) is called
|
153
186
|
- By default, returns HTTP 429 with a JSON error message
|
154
187
|
- Headers are automatically added with limit information
|
155
188
|
- The handler's return value is used (usually render/redirect)
|
156
189
|
|
157
190
|
2. For methods:
|
158
|
-
- The `on_exceeded` handler (
|
191
|
+
- The `on_exceeded` handler (if provided) is called first
|
192
|
+
- Then `RailsRateLimit::RateLimitExceeded` exception is raised
|
159
193
|
- The event is logged if a logger is configured
|
160
|
-
-
|
161
|
-
|
194
|
+
- You should catch the exception to handle the error
|
195
|
+
|
196
|
+
### Default error messages
|
197
|
+
|
198
|
+
By default, the gem logs the error message to the logger together with your custom `on_exceeded` message.
|
199
|
+
```ruby
|
200
|
+
@logger.warn(
|
201
|
+
"Rate limit exceeded for #{key}. " \
|
202
|
+
"Limit: #{limit} requests per #{period} seconds"
|
203
|
+
)
|
204
|
+
# where key for klass is `by` or default unique identifier
|
205
|
+
# Rate limit exceeded for ReportGenerator#generate:object_id=218520. Limit: 2 requests per 10 seconds
|
206
|
+
# Rate limit exceeded for Notification#deliver:id=1. Limit: 3 requests per 60 seconds
|
207
|
+
|
208
|
+
# where key for controller is `by` or default unique identifier
|
209
|
+
# Rate limit exceeded for HomeController:127.0.0.1. Limit: 100 requests per 1 minute
|
210
|
+
```
|
211
|
+
|
212
|
+
You can remove it by setting `config.logger = nil` or specify `by` options.
|
162
213
|
|
163
214
|
### HTTP Headers
|
164
215
|
|
@@ -183,7 +234,7 @@ For controller rate limiting, the following headers are automatically added:
|
|
183
234
|
- Works well in distributed environments
|
184
235
|
- Good option if you're already using Memcached
|
185
236
|
|
186
|
-
### Memory
|
237
|
+
### Memory (Default)
|
187
238
|
- No additional dependencies
|
188
239
|
- Perfect for development or single-server setups
|
189
240
|
- Data is lost on server restart
|
@@ -4,39 +4,40 @@
|
|
4
4
|
module RailsRateLimit
|
5
5
|
class Configuration
|
6
6
|
attr_accessor :default_store, :redis_connection, :memcached_connection, :logger
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :handle_controller_exceeded, :handle_klass_exceeded
|
8
8
|
|
9
9
|
def initialize
|
10
|
-
@default_store = :
|
10
|
+
@default_store = :memory
|
11
11
|
@redis_connection = nil
|
12
12
|
@memcached_connection = nil
|
13
13
|
@logger = nil
|
14
14
|
set_default_handlers
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
18
|
-
raise ArgumentError, "
|
17
|
+
def handle_controller_exceeded=(handler)
|
18
|
+
raise ArgumentError, "handle_controller_exceeded must be a Proc" unless handler.is_a?(Proc)
|
19
19
|
|
20
|
-
@
|
20
|
+
@handle_controller_exceeded = handler
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
raise ArgumentError, "
|
23
|
+
def handle_klass_exceeded=(handler)
|
24
|
+
raise ArgumentError, "handle_klass_exceeded must be a Proc" unless handler.is_a?(Proc)
|
25
25
|
|
26
|
-
@
|
26
|
+
@handle_klass_exceeded = handler
|
27
27
|
end
|
28
28
|
|
29
29
|
private
|
30
30
|
|
31
31
|
def set_default_handlers
|
32
|
-
@
|
32
|
+
@handle_controller_exceeded = lambda {
|
33
33
|
render json: {
|
34
|
-
error: "Too many requests"
|
35
|
-
retry_after: response.headers["Retry-After"]
|
34
|
+
error: "Too many requests"
|
36
35
|
}, status: :too_many_requests
|
37
36
|
}
|
38
37
|
|
39
|
-
@
|
38
|
+
@handle_klass_exceeded = lambda {
|
39
|
+
raise RailsRateLimit::RateLimitExceeded, "Rate limit exceeded"
|
40
|
+
}
|
40
41
|
end
|
41
42
|
end
|
42
43
|
end
|
@@ -13,7 +13,7 @@ module RailsRateLimit
|
|
13
13
|
before_action(options) do |controller|
|
14
14
|
limiter = RateLimiter.new(
|
15
15
|
context: controller,
|
16
|
-
by: by,
|
16
|
+
by: by || "#{controller.class.name}:#{controller.request.remote_ip}",
|
17
17
|
limit: limit,
|
18
18
|
period: period.to_i,
|
19
19
|
store: store
|
@@ -22,7 +22,7 @@ module RailsRateLimit
|
|
22
22
|
begin
|
23
23
|
limiter.perform!
|
24
24
|
rescue RailsRateLimit::RateLimitExceeded
|
25
|
-
handler = on_exceeded.nil? ? RailsRateLimit.configuration.
|
25
|
+
handler = on_exceeded.nil? ? RailsRateLimit.configuration.handle_controller_exceeded : on_exceeded
|
26
26
|
controller.instance_exec(&handler)
|
27
27
|
end
|
28
28
|
end
|
@@ -8,12 +8,50 @@ module RailsRateLimit
|
|
8
8
|
def set_rate_limit(method_name, limit:, period:, by: nil, store: nil, on_exceeded: nil)
|
9
9
|
Validations.validate_options!(limit: limit, period: period, by: by, store: store)
|
10
10
|
|
11
|
+
if method_defined?(method_name) || private_method_defined?(method_name)
|
12
|
+
set_instance_rate_limit(method_name, limit: limit, period: period, by: by, store: store,
|
13
|
+
on_exceeded: on_exceeded)
|
14
|
+
elsif singleton_class.method_defined?(method_name) || singleton_class.private_method_defined?(method_name)
|
15
|
+
set_class_rate_limit(method_name, limit: limit, period: period, by: by, store: store,
|
16
|
+
on_exceeded: on_exceeded)
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Method #{method_name} is not defined"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_instance_rate_limit(method_name, limit:, period:, by:, store:, on_exceeded:)
|
11
23
|
original_method = instance_method(method_name)
|
12
24
|
|
13
25
|
define_method(method_name) do |*args, &block|
|
14
26
|
limiter = RateLimiter.new(
|
15
27
|
context: self,
|
16
|
-
by: by ||
|
28
|
+
by: by || lambda {
|
29
|
+
"#{self.class.name}##{method_name}:#{respond_to?(:id) ? "id=#{id}" : "object_id=#{object_id}"}"
|
30
|
+
},
|
31
|
+
limit: limit,
|
32
|
+
period: period.to_i,
|
33
|
+
store: store
|
34
|
+
)
|
35
|
+
|
36
|
+
begin
|
37
|
+
limiter.perform!
|
38
|
+
original_method.bind(self).call(*args, &block)
|
39
|
+
rescue RailsRateLimit::RateLimitExceeded
|
40
|
+
handler = on_exceeded.nil? ? RailsRateLimit.configuration.handle_klass_exceeded : on_exceeded
|
41
|
+
instance_exec(&handler)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_class_rate_limit(method_name, limit:, period:, by:, store:, on_exceeded:)
|
47
|
+
original_method = singleton_class.instance_method(method_name)
|
48
|
+
|
49
|
+
singleton_class.define_method(method_name) do |*args, &block|
|
50
|
+
limiter = RateLimiter.new(
|
51
|
+
context: self,
|
52
|
+
by: by || lambda {
|
53
|
+
"#{name}.#{method_name}"
|
54
|
+
},
|
17
55
|
limit: limit,
|
18
56
|
period: period.to_i,
|
19
57
|
store: store
|
@@ -23,9 +61,8 @@ module RailsRateLimit
|
|
23
61
|
limiter.perform!
|
24
62
|
original_method.bind(self).call(*args, &block)
|
25
63
|
rescue RailsRateLimit::RateLimitExceeded
|
26
|
-
handler = on_exceeded.nil? ? RailsRateLimit.configuration.
|
64
|
+
handler = on_exceeded.nil? ? RailsRateLimit.configuration.handle_klass_exceeded : on_exceeded
|
27
65
|
instance_exec(&handler)
|
28
|
-
nil
|
29
66
|
end
|
30
67
|
end
|
31
68
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_rate_limit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kasvit
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-01-
|
11
|
+
date: 2025-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dalli
|
@@ -169,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
169
169
|
- !ruby/object:Gem::Version
|
170
170
|
version: '0'
|
171
171
|
requirements: []
|
172
|
-
rubygems_version: 3.
|
172
|
+
rubygems_version: 3.4.10
|
173
173
|
signing_key:
|
174
174
|
specification_version: 4
|
175
175
|
summary: Flexible rate limiting for Rails applications
|