SeleniumHQ/selenium · error · ArgumentError

argument should be a Hash or implement #capabilities

Error message

argument should be a Hash or implement #capabilities

What it means

Raised by Remote::Capabilities#merge! when the argument is neither a Hash nor an object that responds to #capabilities with a Hash result. The method uses respond_to?(:capabilities, true) to detect capabilities-bearing objects and falls back to is_a?(Hash); anything else is rejected to prevent silent corruption of the internal @capabilities hash.

Source

Thrown at rb/lib/selenium/webdriver/remote/capabilities.rb:151

        #
        # Allows setting arbitrary capabilities.
        #

        def []=(key, value)
          @capabilities[key] = value
        end

        def [](key)
          @capabilities[key]
        end

        def merge!(other)
          if other.respond_to?(:capabilities, true) && other.capabilities.is_a?(Hash)
            @capabilities.merge! other.capabilities
          elsif other.is_a? Hash
            @capabilities.merge! other
          else
            raise ArgumentError, 'argument should be a Hash or implement #capabilities'
          end
        end

        def proxy
          @capabilities[:proxy]
        end

        def proxy=(proxy)
          case proxy
          when Hash
            @capabilities[:proxy] = Proxy.new(proxy)
          when Proxy, nil
            @capabilities[:proxy] = proxy
          else
            raise TypeError, "expected Hash or #{Proxy.name}, got #{proxy.inspect}:#{proxy.class}"
          end
        end

View on GitHub (pinned to aa36b38e69)

Solutions

  1. Ensure the argument is a plain Hash: capabilities.merge!({browserName: 'chrome'}).
  2. If merging another Capabilities object, confirm it responds to #capabilities and that method returns a Hash — or call .capabilities on it explicitly first.
  3. If you have an Options object, call .as_json or extract its hash form before merging.
  4. Wrap the argument in a Hash constructor: capabilities.merge!(Hash(other)) only if the object is safely coercible.

Example fix

# before
caps.merge!(driver.options)

# after
caps.merge!(driver.options.as_json)
Defensive patterns

Strategy: validation

Validate before calling

# Validate before merge!:
unless other.is_a?(Hash) || (other.respond_to?(:capabilities, true) && other.capabilities.is_a?(Hash))
  raise ArgumentError, 'merge! requires a Hash or object with #capabilities returning a Hash'
end
capabilities.merge!(other)

Type guard

# Check the argument is mergeable before calling:
def mergeable?(obj)
  obj.is_a?(Hash) || (obj.respond_to?(:capabilities, true) && obj.capabilities.is_a?(Hash))
end

Try / catch

begin
  capabilities.merge!(other)
rescue ArgumentError => e
  # coerce if possible, else report
  capabilities.merge!(other.to_h) if other.respond_to?(:to_h)
end

Prevention

When it happens

Trigger: Calling capabilities.merge!(some_string), capabilities.merge!(an_array), or capabilities.merge!(a_driver_instance). Passing a struct or Data object that holds capability data but does not expose a #capabilities method. Passing nil or an OpenStruct that lacks #capabilities.

Common situations: Attempting to merge a single key-value pair as a two-element array instead of a Hash. Passing a WebDriver::Options instance expecting it to work (Options does not expose #capabilities — it exposes #as_json). Confusing merge! with the constructor which accepts different shapes. Refactoring that changed what object flows into merge!.

Related errors


AI-assisted analysis of SeleniumHQ/selenium@aa36b38e69 (2026-08-14). Data as JSON: /api/errors/c55cb92238fed601. Report an issue: GitHub.