{"record":{"id":"1c7e39fe558980a1","repo":"rack/rack-attack","slug":"must-pass-bantime-option","errorCode":null,"errorMessage":"Must pass bantime option","messagePattern":"Must pass bantime option","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/rack/attack/fail2ban.rb","lineNumber":8,"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","sourceCodeStart":1,"sourceCodeEnd":26,"githubUrl":"https://github.com/rack/rack-attack/blob/b771ea18afb9e6b625906a641d2d331e6d0c282b/lib/rack/attack/fail2ban.rb#L1-L26","documentation":"Fail2Ban.filter(discriminator, options) requires three options: bantime (how long the ban lasts), findtime (counting window) and maxretry (failures before banning). The first line extracts bantime with 'or raise ArgumentError', so the error fires when options[:bantime] is missing - or explicitly nil/false. It is a fail-fast guard because a ban without a duration is meaningless.","triggerScenarios":"Calling Rack::Attack::Fail2Ban.filter(ip, findtime: 60, maxretry: 3) { ... } (bantime omitted), or passing bantime: nil / a misspelled key like ban_time:, inside a Rack::Attack.blocklist block - so it raises on the first request that evaluates the blocklist. Also triggered by direct calls such as Rack::Attack::Fail2Ban.filter('1.2.3.4', {}) { true }.","commonSituations":"Copy-pasting a Fail2Ban example and dropping the bantime line; renaming keys (bantime vs ban_time, or seconds vs ActiveSupport::Duration mistakes like passing 1.hour to a config expecting ints is fine, but passing nil from ENV parsing is not); constructing options dynamically (options = {findtime: x, maxretry: y} then forgetting bantime); earlier rack-attack versions not enforcing all keys, so an upgrade surfaces old configs.","solutions":["Pass all three keys with positive integers (seconds): Rack::Attack::Fail2Ban.filter(req.ip, bantime: 3600, findtime: 600, maxretry: 5) { ... }.","If building options from a hash/config object, merge defaults: opts = {bantime: 3600, findtime: 600, maxretry: 5}.merge(user_opts) so keys can never be absent.","Check for typos in the option keys - the API uses exactly :bantime, :findtime, :maxretry.","Guard ENV-derived values: fail2ban opts like bantime: ENV['BANTIME']&.to_i will be nil when unset - fall back to a literal default."],"exampleFix":"# before\nRack::Attack.blocklist('login abusers') do |req|\n  Rack::Attack::Fail2Ban.filter(req.ip, findtime: 60, maxretry: 3) { req.path == '/login' && req.post? }\nend\n# => ArgumentError: Must pass bantime option\n\n# after\nRack::Attack.blocklist('login abusers') do |req|\n  Rack::Attack::Fail2Ban.filter(req.ip, bantime: 3600, findtime: 60, maxretry: 3) { req.path == '/login' && req.post? }\nend","handlingStrategy":"validation","validationCode":"FAIL2BAN_KEYS = %i[bantime findtime maxretry].freeze\n\nopts = { bantime: 3600, findtime: 600, maxretry: 5 }.merge(user_opts)\nmissing = FAIL2BAN_KEYS.reject { |k| opts[k] }\nraise ArgumentError, \"fail2ban config missing: #{missing.join(', ')}\" unless missing.empty?\n\nRack::Attack::Fail2Ban.filter(ip, opts) { yield_filter }","typeGuard":"def valid_fail2ban_options?(opts)\n  %i[bantime findtime maxretry].all? { |k| opts[k] && opts[k].is_a?(Integer) && opts[k].positive? }\nend","tryCatchPattern":"begin\n  Rack::Attack::Fail2Ban.filter(ip, opts) { bad?(req) }\nrescue ArgumentError => e\n  raise unless e.message.start_with?('Must pass')\n  ErrorReporter.report(e)\n  false # config bug: do not block the client\nend","preventionTips":["Define the option trio once as a frozen constant and reuse it at every filter call site.","Unit-test your initializer: expect Rack::Attack::Fail2Ban.filter('t', opts) { true } not to raise ArgumentError.","Validate ENV-derived numbers with Integer(ENV.fetch('K', default)).","Lint option keys in review - the names mirror the fail2ban daemon but as Ruby symbols."],"tags":["rack-attack","ruby","fail2ban","argument-error","configuration","blocklist"],"backgroundTag":"missing-required-option","analyzedSha":"b771ea18afb9e6b625906a641d2d331e6d0c282b","analyzedAt":"2026-08-21T19:44:17.247Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}