diff --git a/lib/rack/attack/base_proxy.rb b/lib/rack/attack/base_proxy.rb index f10af3d4..354f1902 100644 --- a/lib/rack/attack/base_proxy.rb +++ b/lib/rack/attack/base_proxy.rb @@ -5,6 +5,43 @@ module Rack class Attack class BaseProxy < SimpleDelegator + attr_reader :bypass_all_store_errors, :bypassable_store_errors + + def initialize(store, bypass_all_store_errors: false, bypassable_store_errors: []) + super(store) + @bypass_all_store_errors = bypass_all_store_errors + @bypassable_store_errors = bypassable_store_errors + end + + protected + + def handle_store_error(&block) + yield + rescue => error + if should_bypass_error?(error) + nil + else + raise error + end + end + + private + + def should_bypass_error?(error) + return true if @bypass_all_store_errors + + @bypassable_store_errors.any? do |bypassable_error| + case bypassable_error + when Class + error.is_a?(bypassable_error) + when String + error.class.name == bypassable_error + else + false + end + end + end + class << self def proxies @@proxies ||= [] diff --git a/lib/rack/attack/cache.rb b/lib/rack/attack/cache.rb index 9111ab8a..8e8ac3ac 100644 --- a/lib/rack/attack/cache.rb +++ b/lib/rack/attack/cache.rb @@ -19,10 +19,10 @@ def initialize(store: self.class.default_store) attr_reader :store - def store=(store) + def store=(store, bypass_all_store_errors: false, bypassable_store_errors: []) @store = if (proxy = BaseProxy.lookup(store)) - proxy.new(store) + proxy.new(store, bypass_all_store_errors: bypass_all_store_errors, bypassable_store_errors: bypassable_store_errors) else store end diff --git a/lib/rack/attack/store_proxy/dalli_proxy.rb b/lib/rack/attack/store_proxy/dalli_proxy.rb index 48198bb2..fc283bbf 100644 --- a/lib/rack/attack/store_proxy/dalli_proxy.rb +++ b/lib/rack/attack/store_proxy/dalli_proxy.rb @@ -18,13 +18,13 @@ def self.handle?(store) end end - def initialize(client) - super(client) + def initialize(client, **options) + super(client, **options) stub_with_if_missing end def read(key) - rescuing do + handle_store_error do with do |client| client.get(key) end @@ -32,7 +32,7 @@ def read(key) end def write(key, value, options = {}) - rescuing do + handle_store_error do with do |client| client.set(key, value, options.fetch(:expires_in, 0), raw: true) end @@ -40,7 +40,7 @@ def write(key, value, options = {}) end def increment(key, amount, options = {}) - rescuing do + handle_store_error do with do |client| client.incr(key, amount, options.fetch(:expires_in, 0), amount) end @@ -48,7 +48,7 @@ def increment(key, amount, options = {}) end def delete(key) - rescuing do + handle_store_error do with do |client| client.delete(key) end @@ -67,10 +67,10 @@ def with end end - def rescuing - yield - rescue Dalli::DalliError - nil + def should_bypass_error?(error) + # Dalli-specific default behavior: bypass Dalli errors + return true if defined?(::Dalli::DalliError) && error.is_a?(Dalli::DalliError) + super end end end diff --git a/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb b/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb index f7b66c92..81ae840f 100644 --- a/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +++ b/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb @@ -13,11 +13,11 @@ def self.handle?(store) end def read(name, options = {}) - super(name, options.merge!(raw: true)) + handle_store_error { super(name, options.merge!(raw: true)) } end def write(name, value, options = {}) - super(name, value, options.merge!(raw: true)) + handle_store_error { super(name, value, options.merge!(raw: true)) } end end end diff --git a/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb b/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb index 74f665b5..fdd41bf5 100644 --- a/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +++ b/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb @@ -17,25 +17,25 @@ def increment(name, amount = 1, **options) # So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize # the counter. After that we continue using the original RedisCacheStore#increment. if options[:expires_in] && !read(name) - write(name, amount, options) + handle_store_error { write(name, amount, options) } amount else - super + handle_store_error { super } end end end def read(name, options = {}) - super(name, options.merge!(raw: true)) + handle_store_error { super(name, options.merge!(raw: true)) } end def write(name, value, options = {}) - super(name, value, options.merge!(raw: true)) + handle_store_error { super(name, value, options.merge!(raw: true)) } end def delete_matched(matcher, options = nil) - super(matcher.source, options) + handle_store_error { super(matcher.source, options) } end end end diff --git a/lib/rack/attack/store_proxy/redis_proxy.rb b/lib/rack/attack/store_proxy/redis_proxy.rb index 599213ae..549a917b 100644 --- a/lib/rack/attack/store_proxy/redis_proxy.rb +++ b/lib/rack/attack/store_proxy/redis_proxy.rb @@ -6,12 +6,12 @@ module Rack class Attack module StoreProxy class RedisProxy < BaseProxy - def initialize(*args) + def initialize(store, **options) if Gem::Version.new(Redis::VERSION) < Gem::Version.new("3") warn 'RackAttack requires Redis gem >= 3.0.0.' end - super(*args) + super(store, **options) end def self.handle?(store) @@ -19,19 +19,19 @@ def self.handle?(store) end def read(key) - rescuing { get(key) } + handle_store_error { get(key) } end def write(key, value, options = {}) if (expires_in = options[:expires_in]) - rescuing { setex(key, expires_in, value) } + handle_store_error { setex(key, expires_in, value) } else - rescuing { set(key, value) } + handle_store_error { set(key, value) } end end def increment(key, amount, options = {}) - rescuing do + handle_store_error do pipelined do |redis| redis.incrby(key, amount) redis.expire(key, options[:expires_in]) if options[:expires_in] @@ -40,14 +40,14 @@ def increment(key, amount, options = {}) end def delete(key, _options = {}) - rescuing { del(key) } + handle_store_error { del(key) } end def delete_matched(matcher, _options = nil) cursor = "0" source = matcher.source - rescuing do + handle_store_error do # Fetch keys in batches using SCAN to avoid blocking the Redis server. loop do cursor, keys = scan(cursor, match: source, count: 1000) @@ -59,10 +59,10 @@ def delete_matched(matcher, _options = nil) private - def rescuing - yield - rescue Redis::BaseConnectionError - nil + def should_bypass_error?(error) + # Redis-specific default behavior: bypass Redis connection errors + return true if error.is_a?(Redis::BaseConnectionError) + super end end end diff --git a/lib/rack/attack/store_proxy/redis_store_proxy.rb b/lib/rack/attack/store_proxy/redis_store_proxy.rb index 28557bcb..2f199da9 100644 --- a/lib/rack/attack/store_proxy/redis_store_proxy.rb +++ b/lib/rack/attack/store_proxy/redis_store_proxy.rb @@ -11,14 +11,14 @@ def self.handle?(store) end def read(key) - rescuing { get(key, raw: true) } + handle_store_error { get(key, raw: true) } end def write(key, value, options = {}) if (expires_in = options[:expires_in]) - rescuing { setex(key, expires_in, value, raw: true) } + handle_store_error { setex(key, expires_in, value, raw: true) } else - rescuing { set(key, value, raw: true) } + handle_store_error { set(key, value, raw: true) } end end end