Relax API Rate Limits for Self-Hosted Deployments #2465

Merged
julianojulio merged 1 commits from julianojulio/relax-rate-limits-for-self-hosted into main 2025-07-23 22:10:11 +08:00
5 changed files with 114 additions and 4 deletions

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View 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