rails_rate_limit 0.2.0 → 0.3.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: f84a9dd27c84c9d0519657dcdc6a03da54339a00951fba0e4dfe847e210bc416
4
- data.tar.gz: 68ed631f37784ac86f12e261cd28e99caf879f4b604604f5c0717b9e9a071062
3
+ metadata.gz: 898367675a14db6a2edf98d6e052797f4447896e57bc180702c897372f8c2ab4
4
+ data.tar.gz: 4870de49e9e6895566ff929821b4712d5272efddc2154c474bf7ce76653b4d85
5
5
  SHA512:
6
- metadata.gz: 0d5822c1423345099c7d42a21ca11951720de2f9be00e7858fdf9ffee69b3e970454ed878e57e36c75ee74e49c213a0de1a079f2a476c52c95cdaabefdc34138
7
- data.tar.gz: 736ce16dddb1b5e238e88570cd67cbce3895f2bb51a61a10a5d635112d4fe3d3489a33bf01735ddc67ebf350a669f668c3ce1952324a8ba5145649c0806cc379
6
+ metadata.gz: 651e61eeeb9ca99c8e80b03103d27c18d83e0c1d958d3b8e1dd2b5561537df219be3e9c889f5ac79d615416b9317588be7fb0ed5e0ebffa193386dd98b9db7f2
7
+ data.tar.gz: d19a7092d505a0ded484c14335d12652f621543b001742248e4fc61d23628442624e536346e979ea4701df9ee63e8c59ceca488df8868b1f5749c60dce4dd4ff
data/README.md CHANGED
@@ -9,6 +9,17 @@ For example, if you set a limit of 100 requests per hour, and a user makes 100 r
9
9
 
10
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).
11
11
 
12
+ ## Features
13
+
14
+ - Multiple storage backends (Redis, Memcached, Memory)
15
+ - Sliding window algorithm for accurate rate limiting
16
+ - Support for both controllers and Ruby classes
17
+ - Multiple rate limits for controllers
18
+ - Custom rate limit names and skipping for controllers
19
+ - Flexible configuration options
20
+ - Automatic HTTP headers
21
+ - Custom error handlers
22
+
12
23
  ## Installation
13
24
 
14
25
  Add this line to your application's Gemfile:
@@ -23,46 +34,53 @@ And then execute:
23
34
  $ bundle install
24
35
  ```
25
36
 
37
+ Generate the initializer:
38
+
39
+ ```bash
40
+ $ rails generate rails_rate_limit:install
41
+ ```
42
+
43
+ This will create a configuration file at `config/initializers/rails_rate_limit.rb` with all available options commented out.
44
+
26
45
  ## Configuration
27
46
 
28
- Create an initializer `config/initializers/rails_rate_limit.rb`:
47
+ The generated initializer (`config/initializers/rails_rate_limit.rb`) includes all available configuration options with default values commented out. You can uncomment and modify the options you want to customize:
29
48
 
30
49
  ```ruby
31
50
  RailsRateLimit.configure do |config|
32
- # Optional: Choose your storage backend (default: :memory)
33
- config.default_store = :redis # Available options: :redis, :memcached, :memory
34
-
35
- # Optional: Configure Redis connection (required if using Redis store)
36
- config.redis_connection = Redis.new(
37
- url: ENV['REDIS_URL'],
38
- timeout: 1,
39
- reconnect_attempts: 2
40
- )
41
-
42
- # Optional: Configure Memcached connection (required if using Memcached store)
43
- config.memcached_connection = Dalli::Client.new(
44
- ENV['MEMCACHED_URL'],
45
- { expires_in: 1.day, compress: true }
46
- )
47
-
48
- # Optional: Configure logging
49
- # set `nil` to disable logging
50
- config.logger = Rails.logger
51
-
52
- # Optional: Configure default handler for controllers (HTTP requests)
53
- config.handle_controller_exceeded = -> {
54
- # Default handler returns a JSON response with a 429 status code
55
- render json: {
56
- error: "Too many requests",
57
- retry_after: response.headers["Retry-After"]
58
- }, status: :too_many_requests
59
- }
60
-
61
- # Optional: Configure default handler for methods
51
+ # Choose your storage backend (default: :memory)
52
+ # Available options: :redis, :memcached, :memory
53
+ # config.default_store = :memory
54
+
55
+ # Configure Redis connection (required if using Redis store)
56
+ # config.redis_connection = Redis.new(
57
+ # url: ENV['REDIS_URL'],
58
+ # timeout: 1,
59
+ # reconnect_attempts: 2
60
+ # )
61
+
62
+ # Configure Memcached connection (required if using Memcached store)
63
+ # config.memcached_connection = Dalli::Client.new(
64
+ # ENV['MEMCACHED_URL'],
65
+ # { expires_in: 1.day, compress: true }
66
+ # )
67
+
68
+ # Configure logging (set to nil to disable logging)
69
+ # config.logger = Rails.logger
70
+
71
+ # Configure default handler for controllers (HTTP requests)
72
+ # config.handle_controller_exceeded = -> {
73
+ # render json: {
74
+ # error: "Too many requests",
75
+ # retry_after: response.headers["Retry-After"]
76
+ # }, status: :too_many_requests
77
+ # }
78
+
79
+ # Configure default handler for methods
62
80
  # By default, it raises RailsRateLimit::RateLimitExceeded
63
- config.handle_klass_exceeded = -> {
64
- raise RailsRateLimit::RateLimitExceeded, "Rate limit exceeded"
65
- }
81
+ # config.handle_klass_exceeded = -> {
82
+ # raise RailsRateLimit::RateLimitExceeded, "Rate limit exceeded"
83
+ # }
66
84
  end
67
85
  ```
68
86
 
@@ -73,8 +91,8 @@ end
73
91
  Include the module and set rate limits for your controllers:
74
92
 
75
93
  ```ruby
76
- class ApiController < ApplicationController
77
- include RailsRateLimit::Controller # include this module
94
+ class UsersController < ApplicationController
95
+ include RailsRateLimit::Controller # include this module
78
96
 
79
97
  # Basic usage - limit all actions
80
98
  set_rate_limit limit: 100, # Maximum requests allowed
@@ -93,7 +111,82 @@ class ApiController < ApplicationController
93
111
  plan_limit: current_user.plan.limit,
94
112
  upgrade_url: pricing_url
95
113
  }, status: :too_many_requests
96
- }
114
+ },
115
+ as: :custom_rate_limit # Custom name for rate limit (optional)
116
+ end
117
+ ```
118
+
119
+ ### Multiple Rate Limits
120
+
121
+ You can set multiple rate limits for a single controller or his ancestors. Each rate limit can have its own configuration:
122
+
123
+ ```ruby
124
+ class ApiController < ApplicationController
125
+ include RailsRateLimit::Controller
126
+
127
+ # Global rate limit for all actions
128
+ set_rate_limit limit: 1000,
129
+ period: 1.hour,
130
+ as: :global_rate_limit # Custom name for rate limit (optional)
131
+
132
+ # Stricter limit for write operations
133
+ set_rate_limit only: [:create, :update, :destroy],
134
+ limit: 100,
135
+ period: 1.hour,
136
+ as: :write_operations_limit # Custom name for rate limit (optional)
137
+
138
+ # Custom limit for specific action
139
+ set_rate_limit only: [:expensive_operation],
140
+ limit: 10,
141
+ period: 1.day,
142
+ as: :expensive_operation_limit # Custom name for rate limit (optional)
143
+ end
144
+ ```
145
+
146
+ ### Custom Rate Limit Names
147
+
148
+ You can give your rate limits custom names using the `as` option. This is useful for:
149
+ - Better logging and debugging
150
+ - Skipping specific rate limits
151
+ - Better organization of multiple limits
152
+
153
+ ```ruby
154
+ class ApplicationController < ActionController::API
155
+ include RailsRateLimit::Controller
156
+
157
+ # Global rate limit that applies to all inherited controllers
158
+ set_rate_limit limit: 1000,
159
+ period: 1.hour,
160
+ as: :global_rate_limit
161
+ end
162
+
163
+ class ApiController < ApplicationController
164
+ # Additional limit for API endpoints
165
+ set_rate_limit limit: 100,
166
+ period: 1.minute,
167
+ as: :api_rate_limit
168
+ end
169
+ ```
170
+
171
+ ### Skipping Rate Limits
172
+
173
+ You can skip specific rate limits for certain actions using `skip_before_action`:
174
+
175
+ ```ruby
176
+ class PaymentsController < ApiController
177
+ # Skip global rate limit for webhook endpoint
178
+ skip_before_action :global_rate_limit, only: [:webhook]
179
+
180
+ # Skip API rate limit for status check
181
+ skip_before_action :api_rate_limit, only: [:status]
182
+
183
+ def webhook
184
+ # This action will ignore global rate limit (custom name)
185
+ end
186
+
187
+ def status
188
+ # This action will ignore API rate limit (custom name)
189
+ end
97
190
  end
98
191
  ```
99
192
 
@@ -174,6 +267,7 @@ For both controllers and methods:
174
267
  - `on_exceeded`: (Optional) Custom handler for rate limit exceeded
175
268
 
176
269
  Additional options for controllers:
270
+ - `as`: (Optional) Custom name for rate limit
177
271
  - `only`: (Optional) Array of action names to limit
178
272
  - `except`: (Optional) Array of action names to exclude
179
273
 
@@ -204,6 +298,7 @@ By default, the gem logs the error message to the logger together with your cust
204
298
  # where key for klass is `by` or default unique identifier
205
299
  # Rate limit exceeded for ReportGenerator#generate:object_id=218520. Limit: 2 requests per 10 seconds
206
300
  # Rate limit exceeded for Notification#deliver:id=1. Limit: 3 requests per 60 seconds
301
+ # Rate limit exceeded for ReportGenerator.generate. Limit: 2 requests per 10 seconds
207
302
 
208
303
  # where key for controller is `by` or default unique identifier
209
304
  # Rate limit exceeded for HomeController:127.0.0.1. Limit: 100 requests per 1 minute
@@ -220,6 +315,13 @@ For controller rate limiting, the following headers are automatically added:
220
315
 
221
316
  ## Storage Backends
222
317
 
318
+ ### Memory (Default)
319
+ - No additional dependencies
320
+ - Perfect for development or single-server setups
321
+ - Data is lost on server restart
322
+ - Not suitable for distributed systems
323
+ - Thread-safe implementation
324
+
223
325
  ### Redis
224
326
  - Requires the `redis-rails` gem
225
327
  - Best for distributed systems
@@ -234,13 +336,6 @@ For controller rate limiting, the following headers are automatically added:
234
336
  - Works well in distributed environments
235
337
  - Good option if you're already using Memcached
236
338
 
237
- ### Memory (Default)
238
- - No additional dependencies
239
- - Perfect for development or single-server setups
240
- - Data is lost on server restart
241
- - Not suitable for distributed systems
242
- - Thread-safe implementation
243
-
244
339
  ## Development
245
340
 
246
341
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsRateLimit
6
+ module Generators
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+ desc "Creates a RailsRateLimit initializer file at config/initializers"
10
+
11
+ def copy_initializer
12
+ template "initializer.rb", "config/initializers/rails_rate_limit.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ RailsRateLimit.configure do |config|
4
+ # Choose your storage backend (default: :memory)
5
+ # Available options: :redis, :memcached, :memory
6
+ # config.default_store = :memory
7
+
8
+ # Configure Redis connection (required if using Redis store)
9
+ # config.redis_connection = Redis.new(
10
+ # url: ENV['REDIS_URL'],
11
+ # timeout: 1,
12
+ # reconnect_attempts: 2
13
+ # )
14
+
15
+ # Configure Memcached connection (required if using Memcached store)
16
+ # config.memcached_connection = Dalli::Client.new(
17
+ # ENV['MEMCACHED_URL'],
18
+ # { expires_in: 1.day, compress: true }
19
+ # )
20
+
21
+ # Configure logging (set to nil to disable logging)
22
+ # config.logger = Rails.logger
23
+
24
+ # Configure default handler for controllers (HTTP requests)
25
+ # config.handle_controller_exceeded = -> {
26
+ # render json: {
27
+ # error: "Too many requests",
28
+ # retry_after: response.headers["Retry-After"]
29
+ # }, status: :too_many_requests
30
+ # }
31
+
32
+ # Configure default handler for methods
33
+ # By default, it raises RailsRateLimit::RateLimitExceeded
34
+ # config.handle_klass_exceeded = -> {
35
+ # raise RailsRateLimit::RateLimitExceeded, "Rate limit exceeded"
36
+ # }
37
+ end
@@ -1,31 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
+ require "active_support/core_ext/string/inflections"
5
+ require "active_support/core_ext/class/attribute"
4
6
 
5
7
  module RailsRateLimit
6
8
  module Controller
7
9
  extend ActiveSupport::Concern
8
10
 
11
+ included do
12
+ class_attribute :rate_limits, default: []
13
+ class_attribute :skipped_rate_limits, default: []
14
+ end
15
+
9
16
  class_methods do
10
17
  def set_rate_limit(limit:, period:, by: nil, on_exceeded: nil, store: nil, **options)
11
18
  Validations.validate_options!(limit: limit, period: period, by: by, on_exceeded: on_exceeded, store: store)
12
19
 
13
- before_action(options) do |controller|
14
- limiter = RateLimiter.new(
15
- context: controller,
16
- by: by || "#{controller.class.name}:#{controller.request.remote_ip}",
17
- limit: limit,
18
- period: period.to_i,
19
- store: store
20
- )
21
-
22
- begin
23
- limiter.perform!
24
- rescue RailsRateLimit::RateLimitExceeded
25
- handler = on_exceeded.nil? ? RailsRateLimit.configuration.handle_controller_exceeded : on_exceeded
26
- controller.instance_exec(&handler)
20
+ callback_name = options.delete(:as) || :"check_rate_limit_#{name.to_s.underscore}_#{rate_limits.length}"
21
+
22
+ self.rate_limits = rate_limits + [{
23
+ limit: limit,
24
+ period: period,
25
+ by: by,
26
+ on_exceeded: on_exceeded,
27
+ store: store,
28
+ callback_name: callback_name
29
+ }]
30
+
31
+ define_method(callback_name) do
32
+ self.class.rate_limits.each do |rate_limit|
33
+ next if self.class.skipped_rate_limits.include?(rate_limit[:callback_name])
34
+
35
+ limiter = RateLimiter.new(
36
+ context: self,
37
+ by: rate_limit[:by] || "#{self.class.name}:#{request.remote_ip}",
38
+ limit: rate_limit[:limit],
39
+ period: rate_limit[:period].to_i,
40
+ store: rate_limit[:store]
41
+ )
42
+
43
+ begin
44
+ limiter.perform!
45
+ rescue RailsRateLimit::RateLimitExceeded
46
+ handler = rate_limit[:on_exceeded].nil? ? RailsRateLimit.configuration.handle_controller_exceeded : rate_limit[:on_exceeded]
47
+ return instance_exec(&handler)
48
+ end
27
49
  end
28
50
  end
51
+
52
+ before_action callback_name, **options
53
+ end
54
+
55
+ def skip_before_action(callback_name, **options)
56
+ super
57
+ self.skipped_rate_limits = skipped_rate_limits + [callback_name]
58
+ end
59
+
60
+ def inherited(subclass)
61
+ super
62
+ subclass.rate_limits = rate_limits.dup
63
+ subclass.skipped_rate_limits = skipped_rate_limits.dup
29
64
  end
30
65
  end
31
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsRateLimit
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.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.2.0
4
+ version: 0.3.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-21 00:00:00.000000000 Z
11
+ date: 2025-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dalli
@@ -133,6 +133,8 @@ files:
133
133
  - LICENSE.txt
134
134
  - README.md
135
135
  - Rakefile
136
+ - lib/generators/rails_rate_limit/install/install_generator.rb
137
+ - lib/generators/rails_rate_limit/install/templates/initializer.rb
136
138
  - lib/rails_rate_limit.rb
137
139
  - lib/rails_rate_limit/configuration.rb
138
140
  - lib/rails_rate_limit/controller.rb