{"record":{"id":"e685b04cb5ff7540","repo":"rack/rack-attack","slug":"must-pass-findtime-option","errorCode":null,"errorMessage":"Must pass findtime option","messagePattern":"Must pass findtime option","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/rack/attack/fail2ban.rb","lineNumber":9,"sourceCode":"# frozen_string_literal: true\n\nmodule Rack\n  class Attack\n    class Fail2Ban\n      class << self\n        def filter(discriminator, options)\n          bantime   = options[:bantime]   or raise ArgumentError, \"Must pass bantime option\"\n          findtime  = options[:findtime]  or raise ArgumentError, \"Must pass findtime option\"\n          maxretry  = options[:maxretry]  or raise ArgumentError, \"Must pass maxretry option\"\n\n          if banned?(discriminator)\n            # Return true for blocklist\n            true\n          elsif yield\n            fail!(discriminator, bantime, findtime, maxretry)\n          end\n        end\n\n        def reset(discriminator, options)\n          findtime = options[:findtime] or raise ArgumentError, \"Must pass findtime option\"\n          cache.reset_count(\"#{key_prefix}:count:#{discriminator}\", findtime)\n          # Clear ban flag just in case it's there\n          cache.delete(\"#{key_prefix}:ban:#{discriminator}\")\n        end\n\n        def banned?(discriminator)","sourceCodeStart":1,"sourceCodeEnd":27,"githubUrl":"https://github.com/rack/rack-attack/blob/b771ea18afb9e6b625906a641d2d331e6d0c282b/lib/rack/attack/fail2ban.rb#L1-L27","documentation":"Fail2Ban.filter requires :findtime among its mandatory options - it is the sliding window (in seconds) during which failed attempts are counted toward maxretry. The line 'findtime = options[:findtime] or raise ArgumentError' fires when the key is absent or nil/false. Without findtime the retry counter (cache.count with period findtime) could not bucket counts, so the gem refuses to guess.","triggerScenarios":"Calling Rack::Attack::Fail2Ban.filter(ip, bantime: 3600, maxretry: 5) { ... } inside a blocklist, omitting findtime; passing findtime: nil (e.g. ENV['FINDTIME'] unset and coerced with to_i on nil-safe chain that returns nil); misspelling the key (find_time:, window:). Raises on the first request whose blocklist block runs.","commonSituations":"Trimmed-down copy of the README fail2ban example; config refactors that moved the three options into a YAML/ENV hash and lost one key; version drift from older examples that used positional or differently-named arguments; building the options hash conditionally (only adding findtime in some branch).","solutions":["Add the window: Rack::Attack::Fail2Ban.filter(req.ip, bantime: 3600, findtime: 600, maxretry: 5) { ... }.","Centralize the triple in one constant (FAIL2BAN_OPTS = {bantime: 3600, findtime: 600, maxretry: 5}.freeze) and reuse it for every filter call so one key cannot go missing.","Validate ENV-backed values at boot: findtime: Integer(ENV.fetch('FINDTIME', '600')).","Spell-check keys against the API: exactly :bantime, :findtime, :maxretry."],"exampleFix":"# before\nRack::Attack::Fail2Ban.filter(req.ip, bantime: 3600, maxretry: 5) { unauthorized?(req) }\n# => ArgumentError: Must pass findtime option\n\n# after\nRack::Attack::Fail2Ban.filter(req.ip, bantime: 3600, findtime: 600, maxretry: 5) { unauthorized?(req) }","handlingStrategy":"validation","validationCode":"missing = %i[bantime findtime maxretry].reject { |k| fail2ban_opts[k] }\nraise \"fail2ban opts incomplete: #{missing.inspect}\" unless missing.empty?\nRack::Attack::Fail2Ban.filter(req.ip, fail2ban_opts) { ... }","typeGuard":"def fail2ban_opts_complete?(opts)\n  opts.is_a?(Hash) && %i[bantime findtime maxretry].all? { |k| opts[k].is_a?(Integer) && opts[k].positive? }\nend","tryCatchPattern":"begin\n  Rack::Attack::Fail2Ban.filter(ip, opts) { ... }\nrescue ArgumentError => e\n  raise unless e.message =~ /Must pass :?(bantime|findtime|maxretry) option/\n  log_and_alert(\"fail2ban misconfigured: #{e.message}\")\n  false\nend","preventionTips":["Centralize fail2ban options in one config object loaded from a single YAML/ENV mapping with a schema check at boot.","Test each environment's config (dev/staging/prod) loads complete options - not just dev.","Use Integer(...) coercion with defaults for every numeric option.","Keep filter and reset call sites referencing the same shared constants."],"tags":["rack-attack","ruby","fail2ban","argument-error","configuration","rate-limit"],"backgroundTag":"missing-required-option","analyzedSha":"b771ea18afb9e6b625906a641d2d331e6d0c282b","analyzedAt":"2026-08-21T19:44:17.247Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}