# EncryptedConfiguration#key? Missing Before Rails 7.1

> delegate_missing_to turns a missing predicate into a confusing ArgumentError, not a NoMethodError.

- Author: Curtis Miller
- Published: 2026-09-03
- Categories: rails, ruby, gotchas
- Canonical URL: https://millarian.com/posts/encrypted-configuration-key-predicate-arity/

A gem we were building read Rails encrypted credentials directly. It worked fine on Rails 7.1 and later. On Rails 7.0 it raised:

```
ArgumentError: wrong number of arguments (given 0, expected 1)
```

from three stack frames inside ActiveSupport:

```
active_support/core_ext/module/delegation.rb:304:in `key?'
active_support/core_ext/module/delegation.rb:304:in `public_send'
active_support/core_ext/module/delegation.rb:304:in `method_missing'
```

Nothing in that trace names the line that actually called it.

The bug was a method that doesn't exist yet on the Rails version we were testing.

## The setup

The call was simple:

```ruby
encrypted = ActiveSupport::EncryptedConfiguration.new(
  config_path: config_path,
  key_path: key_path,
  env_key: "SOME_KEY_NOTHING_SETS",
  raise_if_missing_key: true
)

encrypted.key?
```

`#key?` is supposed to answer "is a decryption key available here," without raising the way `#key` does when `raise_if_missing_key` is set. It's the safe way to check before you read.

## Why it happens

`EncryptedFile#key?` was added in Rails 7.1. On 7.0 and earlier the method simply doesn't exist. Rather than a clean `NoMethodError`, `EncryptedConfiguration` declares `delegate_missing_to :options`, so the missing call falls through to the underlying options object, an `ActiveSupport::OrderedOptions`, which behaves like a Hash. `Hash#key?` exists, but it takes one required argument, the key to look up. Called with zero arguments, you get an arity error from a method you never meant to call, on an object you didn't know was involved.

## The fix

Stop calling it. In our case the check deliberately passes an `env_key` that nothing ever sets, so the key file on disk is the only possible source of the key. `key_path.exist?` is exactly equivalent for that purpose, and it behaves the same on every Rails version:

```ruby
key_path.exist? # instead of encrypted.key?
```

Generalize that: when a predicate you want is version dependent, check the underlying condition yourself instead of trusting the method to exist.

If you genuinely need the method itself, `respond_to?` doesn't work here. `delegate_missing_to` also defines `respond_to_missing?` and forwards it to the same target, so `respond_to?(:key?)` returns `true` on 7.0 and the call fails anyway. Ask the class instead, which checks the ancestry rather than `method_missing`:

```ruby
ActiveSupport::EncryptedConfiguration.method_defined?(:key?)
# => true on 7.1, false on 7.0
```

## Takeaway

`delegate_missing_to` converts "this method doesn't exist on this version" into a confusing arity or type error somewhere else entirely. A plain `NoMethodError` tells you what's missing and where. Delegation hides both. If you support multiple framework versions, don't assume a missing method will announce itself as missing, it might announce itself as a different method failing for a different reason. We found this one while running a gem's specs across an Appraisal matrix of ActiveSupport versions.
