rails_rate_limit 0.1.0 → 0.1.2

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: cf52b273c158732c58a654255744cbc096ddab47348f16684211435074dfd9b9
4
+ data.tar.gz: 239cf6c9e06d1300d057c28a2bdcd065de265799d58c369e061e9787e504879b
5
5
  SHA512:
6
- metadata.gz: e77b8544870e4628b9103c017903ed151eb6aa9f501adae15fbdb13a60f5bf79fa62463a6bcdcd856868cd8721e56f7504ce6f9ad4c1dd1f2789343358d4c517
7
- data.tar.gz: d9c0ead4ac6e247c0516e6a31872ec2094efde122226809c51db1761698fcc16b93aea8e5128cb1de06e5f6ace53442ef1f0689248ae42858f4818fb244c1c1b
6
+ metadata.gz: 17198b2b17fe32b8dea0d56d99a02157cc4e0f50e103006b6faea197d38b2206c9810243cdf06a1c0e3c95807bd63d00feea07a426f358f026d60c7c57a5e37b
7
+ data.tar.gz: b1b66ee43016fd1db38083833228c779507dd586573cd9d47a3d69bd0a5cac45e6c478308a9e399d370fe5618b9182c657c55e8b09ff0e4ca465a6dbe25a2e6e
data/README.md CHANGED
@@ -3,12 +3,11 @@
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,7 +99,7 @@ end
99
99
 
100
100
  ### Rate Limiting Methods
101
101
 
102
- You can limit any method calls in your classes:
102
+ You can limit any instance method in your classes (class methods are not supported yet):
103
103
 
104
104
  ```ruby
105
105
  class ApiClient
@@ -109,7 +109,7 @@ class ApiClient
109
109
  # Your API call logic here
110
110
  end
111
111
 
112
- # IMPORTANT: set_rate_limit must be called AFTER method definition
112
+ # IMPORTANT: set_rate_limit must be called AFTER method definition
113
113
  # Basic usage
114
114
  set_rate_limit :make_request,
115
115
  limit: 100,
@@ -122,10 +122,27 @@ class ApiClient
122
122
  by: -> { "client:#{id}" }, # Method call identifier
123
123
  store: :memcached, # Override default store
124
124
  on_exceeded: -> { # Custom error handler
125
+ # You can handle the error here and return any value (including nil)
125
126
  notify_admin
126
127
  log_exceeded_event
127
- enqueue_retry_job
128
+ nil # Method will return nil
128
129
  }
130
+
131
+ # Example with default handler that raises error
132
+ set_rate_limit :risky_method,
133
+ limit: 5,
134
+ period: 1.minute
135
+ # Without on_exceeded option it will use default handler
136
+ # that raises RailsRateLimit::RateLimitExceeded
137
+
138
+ # Example with custom error handling
139
+ def safe_request
140
+ risky_method
141
+ rescue RailsRateLimit::RateLimitExceeded => e
142
+ # Handle the error
143
+ Rails.logger.warn("Rate limit exceeded: #{e.message}")
144
+ nil # or any other fallback value
145
+ end
129
146
  end
130
147
  ```
131
148
 
@@ -135,8 +152,8 @@ For both controllers and methods:
135
152
  - `limit`: (Required) Maximum number of requests/calls allowed
136
153
  - `period`: (Required) Time period for the limit (in seconds or ActiveSupport::Duration)
137
154
  - `by`: (Optional) Lambda/Proc to generate unique identifier
138
- - Default for controllers: `request.remote_ip`
139
- - Default for methods: `"#{class_name}:#{id || object_id}"`
155
+ - Default for controllers: `"#{controller.class.name}:#{controller.request.remote_ip}"`
156
+ - Default for methods: `"#{self.class.name}##{method_name}:#{respond_to?(:id) ? 'id='+id.to_s : 'object_id='+object_id.to_s}"`
140
157
  - `store`: (Optional) Override default storage backend (`:redis`, `:memcached`, `:memory`)
141
158
  - `on_exceeded`: (Optional) Custom handler for rate limit exceeded
142
159
 
@@ -149,16 +166,34 @@ Additional options for controllers:
149
166
  The gem provides different default behaviors for controllers and methods:
150
167
 
151
168
  1. For controllers (HTTP requests):
152
- - The `on_exceeded` handler (or `default_on_controller_exceeded` if not specified) is called
169
+ - The `on_exceeded` handler (or default handler) is called
153
170
  - By default, returns HTTP 429 with a JSON error message
154
171
  - Headers are automatically added with limit information
155
172
  - The handler's return value is used (usually render/redirect)
156
173
 
157
174
  2. For methods:
158
- - The `on_exceeded` handler (or `default_on_method_exceeded` if not specified) is called
175
+ - The `on_exceeded` handler (if provided) is called first
176
+ - Then `RailsRateLimit::RateLimitExceeded` exception is raised
159
177
  - 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
178
+ - You should catch the exception to handle the error
179
+
180
+ ### Default error messages
181
+
182
+ By default, the gem logs the error message to the logger together with your custom `on_exceeded` message.
183
+ ```ruby
184
+ @logger.warn(
185
+ "Rate limit exceeded for #{key}. " \
186
+ "Limit: #{limit} requests per #{period} seconds"
187
+ )
188
+ # where key for klass is `by` or default unique identifier
189
+ # Rate limit exceeded for ReportGenerator#generate:object_id=218520. Limit: 2 requests per 10 seconds
190
+ # Rate limit exceeded for Notification#deliver:id=1. Limit: 3 requests per 60 seconds
191
+
192
+ # where key for controller is `by` or default unique identifier
193
+ # Rate limit exceeded for HomeController:127.0.0.1. Limit: 100 requests per 1 minute
194
+ ```
195
+
196
+ You can remove it by setting `config.logger = nil` or specify `by` options.
162
197
 
163
198
  ### HTTP Headers
164
199
 
@@ -183,7 +218,7 @@ For controller rate limiting, the following headers are automatically added:
183
218
  - Works well in distributed environments
184
219
  - Good option if you're already using Memcached
185
220
 
186
- ### Memory
221
+ ### Memory (Default)
187
222
  - No additional dependencies
188
223
  - Perfect for development or single-server setups
189
224
  - 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
@@ -13,7 +13,9 @@ module RailsRateLimit
13
13
  define_method(method_name) do |*args, &block|
14
14
  limiter = RateLimiter.new(
15
15
  context: self,
16
- by: by || -> { "#{self.class.name}:#{respond_to?(:id) ? id : object_id}" },
16
+ by: by || lambda {
17
+ "#{self.class.name}##{method_name}:#{respond_to?(:id) ? "id=#{id}" : "object_id=#{object_id}"}"
18
+ },
17
19
  limit: limit,
18
20
  period: period.to_i,
19
21
  store: store
@@ -23,9 +25,8 @@ module RailsRateLimit
23
25
  limiter.perform!
24
26
  original_method.bind(self).call(*args, &block)
25
27
  rescue RailsRateLimit::RateLimitExceeded
26
- handler = on_exceeded.nil? ? RailsRateLimit.configuration.default_on_method_exceeded : on_exceeded
28
+ handler = on_exceeded.nil? ? RailsRateLimit.configuration.handle_klass_exceeded : on_exceeded
27
29
  instance_exec(&handler)
28
- nil
29
30
  end
30
31
  end
31
32
  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.1.2"
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.1.2
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