Relax API Rate Limits for Self-Hosted Deployments #2465
@@ -98,7 +98,7 @@ class Api::V1::BaseController < ApplicationController
|
||||
@current_user = @api_key.user
|
||||
@api_key.update_last_used!
|
||||
@authentication_method = :api_key
|
||||
@rate_limiter = ApiRateLimiter.new(@api_key)
|
||||
@rate_limiter = ApiRateLimiter.limit(@api_key)
|
||||
setup_current_context_for_api
|
||||
true
|
||||
end
|
||||
|
||||
@@ -67,7 +67,17 @@ class ApiRateLimiter
|
||||
|
||||
# Class method to get usage for an API key without incrementing
|
||||
def self.usage_for(api_key)
|
||||
new(api_key).usage_info
|
||||
limit(api_key).usage_info
|
||||
end
|
||||
|
||||
def self.limit(api_key)
|
||||
if Rails.application.config.app_mode.self_hosted?
|
||||
# Use NoopApiRateLimiter for self-hosted mode
|
||||
# This means no rate limiting is applied
|
||||
NoopApiRateLimiter.new(api_key)
|
||||
else
|
||||
new(api_key)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
39
app/services/noop_api_rate_limiter.rb
Normal file
39
app/services/noop_api_rate_limiter.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
class NoopApiRateLimiter
|
||||
def initialize(api_key)
|
||||
@api_key = api_key
|
||||
end
|
||||
|
||||
def rate_limit_exceeded?
|
||||
false
|
||||
end
|
||||
|
||||
def increment_request_count!
|
||||
# No operation
|
||||
end
|
||||
|
||||
def current_count
|
||||
0
|
||||
end
|
||||
|
||||
def rate_limit
|
||||
Float::INFINITY
|
||||
end
|
||||
|
||||
def reset_time
|
||||
0
|
||||
end
|
||||
|
||||
def usage_info
|
||||
{
|
||||
current_count: 0,
|
||||
rate_limit: Float::INFINITY,
|
||||
remaining: Float::INFINITY,
|
||||
reset_time: 0,
|
||||
tier: :noop
|
||||
}
|
||||
end
|
||||
|
||||
def self.usage_for(api_key)
|
||||
new(api_key).usage_info
|
||||
end
|
||||
end
|
||||
@@ -9,8 +9,11 @@ class Rack::Attack
|
||||
request.ip if request.path == "/oauth/token"
|
||||
end
|
||||
|
||||
# Determine limits based on self-hosted mode
|
||||
self_hosted = Rails.application.config.app_mode.self_hosted?
|
||||
|
||||
# Throttle API requests per access token
|
||||
throttle("api/requests", limit: 100, period: 1.hour) do |request|
|
||||
throttle("api/requests", limit: self_hosted ? 10_000 : 100, period: 1.hour) do |request|
|
||||
if request.path.start_with?("/api/")
|
||||
# Extract access token from Authorization header
|
||||
auth_header = request.get_header("HTTP_AUTHORIZATION")
|
||||
@@ -25,7 +28,7 @@ class Rack::Attack
|
||||
end
|
||||
|
||||
# More permissive throttling for API requests by IP (for development/testing)
|
||||
throttle("api/ip", limit: 200, period: 1.hour) do |request|
|
||||
throttle("api/ip", limit: self_hosted ? 20_000 : 200, period: 1.hour) do |request|
|
||||
request.ip if request.path.start_with?("/api/")
|
||||
end
|
||||
|
||||
|
||||
58
test/services/noop_api_rate_limiter_test.rb
Normal file
58
test/services/noop_api_rate_limiter_test.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
require "test_helper"
|
||||
|
||||
class NoopApiRateLimiterTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@user = users(:family_admin)
|
||||
# Clean up any existing API keys for this user to ensure tests start fresh
|
||||
@user.api_keys.destroy_all
|
||||
|
||||
@api_key = ApiKey.create!(
|
||||
user: @user,
|
||||
name: "Noop Rate Limiter Test Key",
|
||||
scopes: [ "read" ],
|
||||
display_key: "noop_rate_limiter_test_#{SecureRandom.hex(8)}"
|
||||
)
|
||||
@rate_limiter = NoopApiRateLimiter.new(@api_key)
|
||||
end
|
||||
|
||||
test "should never be rate limited" do
|
||||
assert_not @rate_limiter.rate_limit_exceeded?
|
||||
end
|
||||
|
||||
test "should not increment request count" do
|
||||
@rate_limiter.increment_request_count!
|
||||
assert_equal 0, @rate_limiter.current_count
|
||||
end
|
||||
|
||||
test "should always have zero request count" do
|
||||
assert_equal 0, @rate_limiter.current_count
|
||||
end
|
||||
|
||||
test "should have infinite rate limit" do
|
||||
assert_equal Float::INFINITY, @rate_limiter.rate_limit
|
||||
end
|
||||
|
||||
test "should have zero reset time" do
|
||||
assert_equal 0, @rate_limiter.reset_time
|
||||
end
|
||||
|
||||
test "should provide correct usage info" do
|
||||
usage_info = @rate_limiter.usage_info
|
||||
|
||||
assert_equal 0, usage_info[:current_count]
|
||||
assert_equal Float::INFINITY, usage_info[:rate_limit]
|
||||
assert_equal Float::INFINITY, usage_info[:remaining]
|
||||
assert_equal 0, usage_info[:reset_time]
|
||||
assert_equal :noop, usage_info[:tier]
|
||||
end
|
||||
|
||||
test "class method usage_for should work" do
|
||||
usage_info = NoopApiRateLimiter.usage_for(@api_key)
|
||||
|
||||
assert_equal 0, usage_info[:current_count]
|
||||
assert_equal Float::INFINITY, usage_info[:rate_limit]
|
||||
assert_equal Float::INFINITY, usage_info[:remaining]
|
||||
assert_equal 0, usage_info[:reset_time]
|
||||
assert_equal :noop, usage_info[:tier]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user