{"record":{"id":"3943e84b9db4d077","repo":"rack/rack-attack","slug":"rack-attack-missingstoreerror","errorCode":null,"errorMessage":"Rack::Attack::MissingStoreError","messagePattern":"Rack::Attack::MissingStoreError","errorType":"exception","errorClass":"Rack::Attack::MissingStoreError","httpStatus":null,"severity":"critical","filePath":"lib/rack/attack/cache.rb","lineNumber":40,"sourceCode":"      def store=(store)\n        @store =\n          if (proxy = BaseProxy.lookup(store))\n            proxy.new(store)\n          else\n            store\n          end\n        if @store\n          check_store_methods_presence(:read, :write, :delete, :increment)\n        end\n      end\n\n      def count(unprefixed_key, period)\n        key, expires_in = key_and_expiry(unprefixed_key, period)\n        do_count(key, expires_in)\n      end\n\n      def read(unprefixed_key)\n        raise Rack::Attack::MissingStoreError if store.nil?\n\n        store.read(\"#{prefix}:#{unprefixed_key}\")\n      end\n\n      def write(unprefixed_key, value, expires_in)\n        raise Rack::Attack::MissingStoreError if store.nil?\n\n        store.write(\"#{prefix}:#{unprefixed_key}\", value, expires_in: expires_in)\n      end\n\n      def reset_count(unprefixed_key, period)\n        key, _ = key_and_expiry(unprefixed_key, period)\n        store.delete(key)\n      end\n\n      def delete(unprefixed_key)\n        store.delete(\"#{prefix}:#{unprefixed_key}\")\n      end","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/rack/rack-attack/blob/b771ea18afb9e6b625906a641d2d331e6d0c282b/lib/rack/attack/cache.rb#L22-L58","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nRack::Attack.blocklist('pentest') do |req|\n  Rack::Attack::Fail2Ban.filter(req.ip, bantime: 60, findtime: 60, maxretry: 3) { req.path =~ /^/admin/ }\nend\n# => Rack::Attack::MissingStoreError (store is nil, Fail2Ban.banned? calls cache.read)\n\n# after\n# config/initializers/rack_attack.rb\nRack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV.fetch('REDIS_URL'))\nRack::Attack.blocklist('pentest') do |req|\n  Rack::Attack::Fail2Ban.filter(req.ip, bantime: 60, findtime: 60, maxretry: 3) { req.path =~ /^/admin/ }\nend","handlingStrategy":"validation","validationCode":"# Run at boot, after rack-attack setup and before serving traffic\nunless Rack::Attack.cache.store\n  raise 'rack-attack: no cache store configured - set Rack::Attack.cache.store (Redis/Memcached/Rails.cache)'\nend\n\n# Minimal store compatibility check (mirrors Cache#check_store_methods_presence)\nmissing = %i[read write delete increment].reject { |m| Rack::Attack.cache.store.respond_to?(m) }\nabort \"rack-attack store missing #{missing.join(', ')}\" unless missing.empty?","typeGuard":"# Ruby predicate you can branch on before enabling cache-backed rules\ndef rack_attack_store_configured?\n  store = Rack::Attack.cache.store\n  !store.nil? && %i[read write delete increment].all? { |m| store.respond_to?(m) }\nend","tryCatchPattern":"begin\n  banned = Rack::Attack::Fail2Ban.banned?(ip)\nrescue Rack::Attack::MissingStoreError => e\n  Rails.logger.error(\"rack-attack store missing: #{e.message}\")\n  banned = false # explicit fail-open decision; prefer fixing config instead\n  raise if Rails.env.test? # never hide it in tests\nend","preventionTips":["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."],"tags":["rack-attack","ruby","cache","configuration","missing-store","fail2ban","throttling"],"backgroundTag":"cache-store-not-configured","analyzedSha":"b771ea18afb9e6b625906a641d2d331e6d0c282b","analyzedAt":"2026-08-21T19:44:17.247Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}