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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e681ccbf67051878bd884a7a55db8650056d4107a78b6c5c8a40d5bd5576e50e
4
- data.tar.gz: 3ce8f1ddde50ca6cf278f3431b1e263d147631c4081be305447a57d38d3f4bd4
3
+ metadata.gz: f84a9dd27c84c9d0519657dcdc6a03da54339a00951fba0e4dfe847e210bc416
4
+ data.tar.gz: 68ed631f37784ac86f12e261cd28e99caf879f4b604604f5c0717b9e9a071062
5
5
  SHA512:
6
- metadata.gz: e77b8544870e4628b9103c017903ed151eb6aa9f501adae15fbdb13a60f5bf79fa62463a6bcdcd856868cd8721e56f7504ce6f9ad4c1dd1f2789343358d4c517
7
- data.tar.gz: d9c0ead4ac6e247c0516e6a31872ec2094efde122226809c51db1761698fcc16b93aea8e5128cb1de06e5f6ace53442ef1f0689248ae42858f4818fb244c1c1b
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
  [![Gem Version](https://badge.fury.io/rb/rails_rate_limit.svg)](https://badge.fury.io/rb/rails_rate_limit)
4
4
  [![Build Status](https://github.com/kasvit/rails_rate_limit/workflows/Ruby/badge.svg)](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
- Supports rate limiting for both HTTP requests and method calls, with multiple storage backends (Redis, Memcached, Memory).
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
- # Required: Choose your default storage backend
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.default_on_controller_exceeded = -> {
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
- config.default_on_method_exceeded = -> {
62
- # Your default logic for methods
63
- # For example: log the event, notify admins, etc.
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 any method calls in your classes:
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 # include this module
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
- # IMPORTANT: set_rate_limit must be called AFTER method definition
113
- # Basic usage
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
- # Advanced usage with all options
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
- enqueue_retry_job
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: `"#{class_name}:#{id || object_id}"`
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 `default_on_controller_exceeded` if not specified) is called
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 (or `default_on_method_exceeded` if not specified) is called
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
- - Returns `nil` after handler execution to indicate no result
161
- - No exception is raised, making it easier to handle in your code
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 :default_on_controller_exceeded, :default_on_method_exceeded
7
+ attr_reader :handle_controller_exceeded, :handle_klass_exceeded
8
8
 
9
9
  def initialize
10
- @default_store = :redis
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 default_on_controller_exceeded=(handler)
18
- raise ArgumentError, "default_on_controller_exceeded must be a Proc" unless handler.is_a?(Proc)
17
+ def handle_controller_exceeded=(handler)
18
+ raise ArgumentError, "handle_controller_exceeded must be a Proc" unless handler.is_a?(Proc)
19
19
 
20
- @default_on_controller_exceeded = handler
20
+ @handle_controller_exceeded = handler
21
21
  end
22
22
 
23
- def default_on_method_exceeded=(handler)
24
- raise ArgumentError, "default_on_method_exceeded must be a Proc" unless handler.is_a?(Proc)
23
+ def handle_klass_exceeded=(handler)
24
+ raise ArgumentError, "handle_klass_exceeded must be a Proc" unless handler.is_a?(Proc)
25
25
 
26
- @default_on_method_exceeded = handler
26
+ @handle_klass_exceeded = handler
27
27
  end
28
28
 
29
29
  private
30
30
 
31
31
  def set_default_handlers
32
- @default_on_controller_exceeded = lambda {
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
- @default_on_method_exceeded = -> { nil }
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.default_on_controller_exceeded : on_exceeded
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 || -> { "#{self.class.name}:#{respond_to?(:id) ? id : object_id}" },
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.default_on_method_exceeded : on_exceeded
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
@@ -48,8 +48,6 @@ module RailsRateLimit
48
48
  end
49
49
 
50
50
  def resolve_key
51
- return context.request.remote_ip if by.nil?
52
-
53
51
  by.is_a?(Proc) ? context.instance_exec(&by) : by
54
52
  end
55
53
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsRateLimit
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  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.1.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-16 00:00:00.000000000 Z
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.3.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