rack/rack-attack · error · ArgumentError

Must pass findtime option

Error message

Must pass findtime option

What it means

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.

Source

Thrown at lib/rack/attack/fail2ban.rb:9

# frozen_string_literal: true

module Rack
  class Attack
    class Fail2Ban
      class << self
        def filter(discriminator, options)
          bantime   = options[:bantime]   or raise ArgumentError, "Must pass bantime option"
          findtime  = options[:findtime]  or raise ArgumentError, "Must pass findtime option"
          maxretry  = options[:maxretry]  or raise ArgumentError, "Must pass maxretry option"

          if banned?(discriminator)
            # Return true for blocklist
            true
          elsif yield
            fail!(discriminator, bantime, findtime, maxretry)
          end
        end

        def reset(discriminator, options)
          findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option"
          cache.reset_count("#{key_prefix}:count:#{discriminator}", findtime)
          # Clear ban flag just in case it's there
          cache.delete("#{key_prefix}:ban:#{discriminator}")
        end

        def banned?(discriminator)

View on GitHub (pinned to b771ea18af)

Solutions

  1. Add the window: Rack::Attack::Fail2Ban.filter(req.ip, bantime: 3600, findtime: 600, maxretry: 5) { ... }.
  2. 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.
  3. Validate ENV-backed values at boot: findtime: Integer(ENV.fetch('FINDTIME', '600')).
  4. Spell-check keys against the API: exactly :bantime, :findtime, :maxretry.

Example fix

# before
Rack::Attack::Fail2Ban.filter(req.ip, bantime: 3600, maxretry: 5) { unauthorized?(req) }
# => ArgumentError: Must pass findtime option

# after
Rack::Attack::Fail2Ban.filter(req.ip, bantime: 3600, findtime: 600, maxretry: 5) { unauthorized?(req) }
Defensive patterns

Strategy: validation

Validate before calling

missing = %i[bantime findtime maxretry].reject { |k| fail2ban_opts[k] }
raise "fail2ban opts incomplete: #{missing.inspect}" unless missing.empty?
Rack::Attack::Fail2Ban.filter(req.ip, fail2ban_opts) { ... }

Type guard

def fail2ban_opts_complete?(opts)
  opts.is_a?(Hash) && %i[bantime findtime maxretry].all? { |k| opts[k].is_a?(Integer) && opts[k].positive? }
end

Try / catch

begin
  Rack::Attack::Fail2Ban.filter(ip, opts) { ... }
rescue ArgumentError => e
  raise unless e.message =~ /Must pass :?(bantime|findtime|maxretry) option/
  log_and_alert("fail2ban misconfigured: #{e.message}")
  false
end

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of rack/rack-attack@b771ea18af (2026-08-21). Data as JSON: /api/errors/e685b04cb5ff7540. Report an issue: GitHub.