rack/rack-attack · critical · Rack::Attack::MissingStoreError
Rack::Attack::MissingStoreError
Error message
Rack::Attack::MissingStoreError
What it means
Rack::Attack keeps all throttle counters, Fail2Ban bans and allow2ban state in a shared cache store (Redis, Memcached, or Rails.cache). Cache#read raises Rack::Attack::MissingStoreError when the read path runs while Rack::Attack.cache.store is nil - i.e. no store was ever assigned and Cache.default_store found no Rails.cache. The gem raises instead of returning nil so that cache-backed rules (e.g. Fail2Ban.banned?) never silently report 'not banned' when counting is impossible.
Source
Thrown at lib/rack/attack/cache.rb:40
def store=(store)
@store =
if (proxy = BaseProxy.lookup(store))
proxy.new(store)
else
store
end
if @store
check_store_methods_presence(:read, :write, :delete, :increment)
end
end
def count(unprefixed_key, period)
key, expires_in = key_and_expiry(unprefixed_key, period)
do_count(key, expires_in)
end
def read(unprefixed_key)
raise Rack::Attack::MissingStoreError if store.nil?
store.read("#{prefix}:#{unprefixed_key}")
end
def write(unprefixed_key, value, expires_in)
raise Rack::Attack::MissingStoreError if store.nil?
store.write("#{prefix}:#{unprefixed_key}", value, expires_in: expires_in)
end
def reset_count(unprefixed_key, period)
key, _ = key_and_expiry(unprefixed_key, period)
store.delete(key)
end
def delete(unprefixed_key)
store.delete("#{prefix}:#{unprefixed_key}")
endView on GitHub (pinned to b771ea18af)
Solutions
- Set a cache store in your rack-attack initializer: Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL']) (or MemCacheStore/DalliStore for memcached).
- In Rails, either rely on config.cache_store being set before the initializer runs, or assign it explicitly: Rack::Attack.cache.store = ::Rails.cache.
- For tests or single-process apps, use ActiveSupport::Cache::MemoryStore.new (remember counters will not be shared across processes/threads depending on store).
- If you never intend to throttle, remove the Fail2Ban/safelist rules that touch cache.read so the read path is never hit.
Example fix
# before
Rack::Attack.blocklist('pentest') do |req|
Rack::Attack::Fail2Ban.filter(req.ip, bantime: 60, findtime: 60, maxretry: 3) { req.path =~ /^/admin/ }
end
# => Rack::Attack::MissingStoreError (store is nil, Fail2Ban.banned? calls cache.read)
# after
# config/initializers/rack_attack.rb
Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV.fetch('REDIS_URL'))
Rack::Attack.blocklist('pentest') do |req|
Rack::Attack::Fail2Ban.filter(req.ip, bantime: 60, findtime: 60, maxretry: 3) { req.path =~ /^/admin/ }
end Defensive patterns
Strategy: validation
Validate before calling
# Run at boot, after rack-attack setup and before serving traffic
unless Rack::Attack.cache.store
raise 'rack-attack: no cache store configured - set Rack::Attack.cache.store (Redis/Memcached/Rails.cache)'
end
# Minimal store compatibility check (mirrors Cache#check_store_methods_presence)
missing = %i[read write delete increment].reject { |m| Rack::Attack.cache.store.respond_to?(m) }
abort "rack-attack store missing #{missing.join(', ')}" unless missing.empty? Type guard
# Ruby predicate you can branch on before enabling cache-backed rules
def rack_attack_store_configured?
store = Rack::Attack.cache.store
!store.nil? && %i[read write delete increment].all? { |m| store.respond_to?(m) }
end Try / catch
begin
banned = Rack::Attack::Fail2Ban.banned?(ip)
rescue Rack::Attack::MissingStoreError => e
Rails.logger.error("rack-attack store missing: #{e.message}")
banned = false # explicit fail-open decision; prefer fixing config instead
raise if Rails.env.test? # never hide it in tests
end Prevention
- Set Rack::Attack.cache.store in the same initializer file that defines throttle/fail2ban rules so they are never deployed apart.
- Add a boot assertion (raise if store is nil when cache-backed rules exist) to CI and deploy checks.
- In Rails, assign Rack::Attack.cache.store = ::Rails.cache explicitly instead of relying on load order of Cache.default_store.
- Use a real shared store (Redis/Memcached) in production; MemoryStore only for tests, since per-process counters under-count.
When it happens
Trigger: Any code path that calls Rack::Attack.cache.read with no store configured: Rack::Attack::Fail2Ban.banned?(ip) (reads the 'fail2ban:ban:<ip>' key) or Fail2Ban.filter's banned? check inside a blocklist, or direct Rack::Attack.cache.read('key') calls. Store is nil when Rack::Attack.cache.store was never set and Rails is not defined (or Rails.cache is nil) when the Cache object is instantiated.
Common situations: Plain Rack/Sinatra apps that mount Rack::Attack middleware but skip the cache.store configuration step documented for non-Rails setups; test suites that load the gem without Rails.cache; Rails apps where the middleware/initializer runs before Rails.cache is available, so Cache.default_store returns nil; upgrading to rack-attack 6.x where the old NoMethodError on nil store became this explicit error.
Related errors
- Must pass bantime option
- Must pass findtime option
- Must pass maxretry option
- Must pass #{opt.inspect} option
AI-assisted analysis of rack/rack-attack@b771ea18af (2026-08-21).
Data as JSON: /api/errors/3943e84b9db4d077.
Report an issue: GitHub.