Skip to content

Add Grape.config.allow_undeclared_params; false answers 400 to undeclared params - #3054

Merged
ericproulx merged 1 commit into
masterfrom
feature/strict-params
Nov 8, 2026
Merged

ericproulx merged 1 commit into
masterfrom
feature/strict-params

Conversation

@ericproulx

@ericproulx ericproulx commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2241. Refs #2416.

Adds Grape.config.allow_undeclared_params. Set to false, a request carrying a param its endpoint did not declare is answered 400. It defaults to true, which keeps today's behaviour, and Grape 5.0 makes false the default.

Grape.config.allow_undeclared_params = false

params do
  requires :name, type: String
  optional :address, type: Hash do
    requires :street, type: String
  end
end
post '/users' do
  # ...
end
POST /users {"name":"Bob","admin":true,"address":{"street":"Main","zip":"1"}}
=> 400 {"error":"admin is not allowed, address[zip] is not allowed"}

Behaviour

  • Undeclared params are raised in the same Grape::Exceptions::ValidationErrors as the other validation errors, so rescue_from and error formatting work unchanged. The message is "is not allowed" (grape.errors.messages.undeclared), the same wording dry-schema uses for unknown keys.
  • Rejected rather than dropped. Dropping them would change what params returns in every endpoint, and it would hide client typos (Fail on (or get list of) unknown parameters #2241).
  • What counts as declared:
    • The names in the endpoint's params blocks, its own and its namespaces'. That includes given blocks; a param renamed with as: counts by the name the client sends.
    • A contract's keys.
    • The route's path params.
    • format.
  • Hash, Array and JSON params are checked key by key, and an undeclared key is named by its path, e.g. items[1][price].
    • A param declared without a block, such as type: Hash or Array[String], takes anything.
    • A param declared in several given blocks accepts the keys of every declaration.
  • The check runs after the validators, so a JSON param sent as a String is checked as the Hash it is coerced to.
  • A mounted API's endpoints are checked. A mounted Rack app is not.

Implementation

  • Grape::Validations::UndeclaredParams builds the declared names into a frozen tree at compile!. The sources are route_declared_params, each contract's key_map.dump, and the route regexp's capture names. The tree is held in Endpoint::State#undeclared_params, which is nil while the setting is true.
  • Endpoint#run_validators adds its errors to the validators' errors.
  • Request#params_sent? lets a request with no query string and no body through without building params.

Performance

These are paired medians. "true" compares this branch with master; "false" compares the setting false with it true on this branch.

bare GET POST, 4 params
true, interpreter 1.000 0.991
true, YJIT 1.004 1.013
false, interpreter 0.930 0.953
false, YJIT 0.934 0.938

With the setting true, there is no measurable cost.

Putting path params into the tree at compile time matters for speed. Checking routing args per request sent every :id route down the slow path, at about +2.8 µs.

Known limits (documented in the README)

  • The contents of a oneof: param, and of a Hash, JSON or File param declared without a block, are not checked. declared(params) treats them the same way.
  • A contract that passes replaces each Array of Hashes with its own output. An undeclared key inside one is therefore dropped rather than reported.
  • With version ..., using: :param, the version param counts as undeclared when it reaches an unversioned endpoint.
  • With the setting false, a malformed query string gets a 400 even on an endpoint that never reads params, because the check has to parse it.

Towards 5.0

Setting it to false across this repo's suite gives 90 failures. All of them are specs that send params they never declare. A common case is a params block written for a get whose sibling put declares nothing.

Apps will hit the same thing, especially with tokens that only before filters read. The UPGRADING entry tells users to set it to false in their test suite now and to declare such params. In 5.0 they can set it to true to keep today's behaviour.

Tests

  • spec/grape/validations/undeclared_params_spec.rb: 19 examples.
  • A dry-validation contract context in spec/integration/dry_validation/dry_validation_spec.rb.
  • bundle exec rake is green. The dry_validation gemfile suite is green too.

🤖 Generated with Claude Code

@ericproulx
ericproulx force-pushed the feature/strict-params branch from f4d2ad0 to e0b1a15 Compare November 3, 2026 08:57
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@dblock

dblock commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

I’m an AI posting on behalf of @dblock. Could we use a positively phrased option? @dblock prefers positive options. allow_undeclared_params would read naturally, with true preserving the current behavior and false rejecting undeclared params (the planned Grape 5.0 default).

@ericproulx
ericproulx force-pushed the feature/strict-params branch from e717009 to 580d9a6 Compare November 7, 2026 12:44
@ericproulx ericproulx changed the title Add Grape.config.reject_undeclared_params to answer 400 to undeclared params Add Grape.config.allow_undeclared_params; false answers 400 to undeclared params Nov 7, 2026
ericproulx added a commit that referenced this pull request Nov 7, 2026
The spec added in #3051 stores `grape.errors.format` with
`I18n.backend.store_translations`, which does not load the locale files.
When the backend has not loaded them yet (the example runs first, or
right after another spec's `I18n.reload!`), the first lookup loads
en.yml over the stored translations and puts the default format back.
`format` is the only key a spec stores that en.yml also defines, so this
example is the only one affected. It fails on its own
(`rspec spec/grape/exceptions/validation_errors_spec.rb:85`) and under
seed 28071, which failed the rails_7_2 job on #3054.

`I18n.backend.translations(do_init: true)` loads en.yml before the
override is stored. `eager_load!` would too, but it also leaves the
backend flagged as eager-loaded for the rest of the run, so every later
`I18n.reload!` reloads at once, under whatever locales the spec has set
at that moment. base_spec's `after { I18n.reload! }` runs while
`available_locales` is `[:de, :jp]`, so the reload skipped `:en`, and
every translation after it came back as its key: 1 to 80 failures
depending on the seed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ericproulx added a commit that referenced this pull request Nov 7, 2026
The spec added in #3051 stores `grape.errors.format` with
`I18n.backend.store_translations`, which does not load the locale files.
When the backend has not loaded them yet (the example runs first, or
right after another spec's `I18n.reload!`), the first lookup loads
en.yml over the stored translations and puts the default format back.
`format` is the only key a spec stores that en.yml also defines, so this
example is the only one affected. It fails on its own
(`rspec spec/grape/exceptions/validation_errors_spec.rb:85`) and under
seed 28071, which failed the rails_7_2 job on #3054.

`I18n.backend.translations(do_init: true)` loads en.yml before the
override is stored. `eager_load!` would too, but it also leaves the
backend flagged as eager-loaded for the rest of the run, so every later
`I18n.reload!` reloads at once, under whatever locales the spec has set
at that moment. base_spec's `after { I18n.reload! }` runs while
`available_locales` is `[:de, :jp]`, so the reload skipped `:en`, and
every translation after it came back as its key: 1 to 80 failures
depending on the seed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the feature/strict-params branch from 580d9a6 to 1b5c416 Compare November 7, 2026 12:52
@ericproulx
ericproulx force-pushed the feature/strict-params branch 2 times, most recently from d27e783 to 601aeeb Compare November 8, 2026 20:15
…ared params

With the setting false, a request carrying a param its endpoint did not
declare is answered 400, the undeclared params reported in the same
Grape::Exceptions::ValidationErrors as the other validation errors, with
the message "is not allowed".

Declared are the names of the endpoint's params blocks (its own and its
namespaces', given blocks included, renamed params by the name sent), a
contract's keys, the route's path params, and format. Hash, Array and
JSON params are looked into key by key and named by path, as in
items[1][price]. The check runs after the validators, so a JSON param
sent as a String is looked into as the Hash it is coerced to.

The declared names are folded into a frozen tree at compile!, held in
Endpoint::State. A request with no query string and no body is let
through without building params.

The setting defaults to true, which keeps today's behaviour, and Grape
5.0 makes false the default.

Fixes #2241. Refs #2416.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the feature/strict-params branch from 601aeeb to ce09d41 Compare November 8, 2026 20:27
@ericproulx
ericproulx merged commit 846d4ab into master Nov 8, 2026
51 checks passed
@ericproulx
ericproulx deleted the feature/strict-params branch November 8, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fail on (or get list of) unknown parameters

2 participants