{"record":{"id":"5ba474b0660b6c0a","repo":"rack/rack-attack","slug":"must-pass-opt-inspect-option","errorCode":null,"errorMessage":"Must pass #{opt.inspect} option","messagePattern":"Must pass #(.+?) option","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/rack/attack/throttle.rb","lineNumber":14,"sourceCode":"# frozen_string_literal: true\n\nmodule Rack\n  class Attack\n    class Throttle\n      MANDATORY_OPTIONS = [:limit, :period].freeze\n\n      attr_reader :name, :limit, :period, :block, :type\n\n      def initialize(name, options, &block)\n        @name = name\n        @block = block\n        MANDATORY_OPTIONS.each do |opt|\n          raise ArgumentError, \"Must pass #{opt.inspect} option\" unless options[opt]\n        end\n        @limit = options[:limit]\n        @period = options[:period].respond_to?(:call) ? options[:period] : options[:period].to_i\n        @type   = options.fetch(:type, :throttle)\n      end\n\n      def cache\n        Rack::Attack.cache\n      end\n\n      def matched_by?(request)\n        discriminator = discriminator_for(request)\n        return false unless discriminator\n\n        current_period  = period_for(request)\n        current_limit   = limit_for(request)\n        count           = cache.count(\"#{name}:#{discriminator}\", current_period)\n","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/rack/rack-attack/blob/b771ea18afb9e6b625906a641d2d331e6d0c282b/lib/rack/attack/throttle.rb#L1-L32","documentation":"Rack::Attack::Throttle#initialize (the object behind Rack::Attack.throttle(name, options, &block)) enforces MANDATORY_OPTIONS = [:limit, :period] with 'raise ArgumentError, \"Must pass #{opt.inspect} option\" unless options[opt]'. :limit is the max requests per window and :period the window length in seconds (or a callable). The message interpolates the missing symbol, so in practice you see 'Must pass :limit option' or 'Must pass :period option'. The guard is truthiness-based, so nil/false values raise as well (0 is truthy in Ruby and passes).","triggerScenarios":"Rack::Attack.throttle('req/ip') { |req| req.ip } with no options hash, or hashes missing either key: throttle('logins', period: 60) {...} (no limit), throttle('api', limit: 10) {...} (no period). Also limit: nil / period: nil from ENV or YAML config, and misspelled keys (rate:, window:, per:). Raises once at Rails boot / rackup when the initializer defines the throttle, before any request is served.","commonSituations":"First-time setup following a throttle example and dropping a line; extracting limits to ENV/config (ENV['RATE_LIMIT'] unset -> nil); renaming keys during a port from Rack::Ratelimit or redis-throttle whose option names differ (requests:, seconds:); upgrading rack-attack across majors where option names were standardized; dynamic rule DSLs that build options hashes conditionally.","solutions":["Add both mandatory keys: Rack::Attack.throttle('req/ip', limit: 100, period: 60) { |req| req.ip }.","For ENV-driven config, coerce with defaults: limit: Integer(ENV.fetch('RATE_LIMIT', '100')), period: Integer(ENV.fetch('RATE_PERIOD', '60')).","Check key spelling - the API uses exactly :limit and :period (:period may be a lambda/proc of the request).","If wrapping throttle in your own DSL, validate the options hash before delegating so the error names your wrapper, not rack-attack internals."],"exampleFix":"# before\nRack::Attack.throttle('logins per ip') do |req|\n  req.ip if req.path == '/login'\nend\n# => ArgumentError: Must pass :limit option (then :period)\n\n# after\nRack::Attack.throttle('logins per ip', limit: 5, period: 60) do |req|\n  req.ip if req.path == '/login'\nend","handlingStrategy":"validation","validationCode":"def build_throttle(name, opts, &block)\n  missing = %i[limit period].reject { |k| opts[k] }\n  raise ArgumentError, \"throttle '#{name}' missing options: #{missing.inspect}\" unless missing.empty?\n  Rack::Attack.throttle(name, opts, &block)\nend\n\nbuild_throttle('req/ip', limit: Integer(ENV.fetch('RATE_LIMIT', '100')), period: 60) { |req| req.ip }","typeGuard":"def valid_throttle_options?(opts)\n  opts.is_a?(Hash) &&\n    opts[:limit].is_a?(Integer) && opts[:limit].positive? &&\n    (opts[:period].is_a?(Integer) || opts[:period].respond_to?(:call))\nend","tryCatchPattern":"begin\n  Rack::Attack.throttle(name, opts, &block)\nrescue ArgumentError => e\n  raise unless e.message =~ /Must pass :(limit|period) option/\n  abort \"rack-attack config error in '#{name}': #{e.message}\" # fail boot loudly\nend","preventionTips":["Add an initializer spec that loads the whole rack-attack config - missing limit/period then fails CI at boot, not production.","Use Integer(ENV.fetch(..., default)) for numeric options so nil never reaches Throttle#initialize.","Remember period may be a proc (period: ->(req) { ... }); anything nil/false raises even if the key exists.","Copy option names exactly (:limit, :period) when porting rules from other rate-limiting gems."],"tags":["rack-attack","ruby","throttle","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"}