Skip to content

Make a helper reach the routes declared above it in its scope - #3061

Merged
ericproulx merged 1 commit into
masterfrom
fix/helpers-declared-after-routes
Nov 8, 2026
Merged

ericproulx merged 1 commit into
masterfrom
fix/helpers-declared-after-routes

Conversation

@ericproulx

@ericproulx ericproulx commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #34.

Problem

Within one scope, a route declared above a helpers block couldn't call its helpers:

class API < Grape::API
  resource :foo do
    get(:broke) { hello_helper } # NameError
    helpers { def hello_helper = 'Hello World!' }
    get(:works) { hello_helper } # 'Hello World!'
  end
end

The rule only held for the route's own scope, which made it look arbitrary. On master:

helpers declared… reaches a route declared before it?
later in the route's own scope no
later in an enclosing scope yes
after mount yes

The cause is that an endpoint is built from a point-in-time copy of its scope's settings (point_in_time_copy_for_endpoint). The copy keeps the scope's own stackables, helpers included, as they were at the route, but its link to the enclosing scopes stays live.

Fix

The endpoint also keeps the live settings of the scope it was declared in, and build_helpers reads the helpers from them in compile!, by which time the whole class body has run. Nothing else reads them. Validations, callbacks, rescue_from and use still come from the copy, so they keep applying only to the routes declared below them.

  • A helper declared in a sibling namespace still doesn't reach the route.
  • Helpers from enclosing scopes and through mount work as before.

Behaviour change

A helper method defined twice in one scope now answers with its last definition in every route of that scope, including routes declared between the two definitions. It's the usual Ruby rule that the last definition wins. No spec pinned the old behaviour, but it's a contract change, so it gets an UPGRADING entry (4.2.0), with the workarounds: different method names, or separate namespaces. The README no longer lists helpers among the declarations that have to come above the routes they cover.

Warning, until 5.0

The change is silent at runtime, so the endpoint warns about it when it compiles. Its settings copy still holds the helpers the route reached before this PR, so it compares, method by method, the definition the route answered with before against the one it answers with now. It warns only where they differ:

Grape: helper `greeting` is defined again below GET /before in the same scope, so since 4.2.0 that route answers with the later definition (see UPGRADING.md). Rename one of them, or declare the route in its own namespace.

The warning lives in Grape::Endpoint::RedefinedHelpersWarning, so it goes away in 5.0 by deleting one file. That also keeps Endpoint under its Metrics/ClassLength limit, which it went over once rebased onto #3054.

These stay quiet:

Private helpers are covered too. The check runs at compile time only, never per request. It goes in 5.0.

params :name helpers used with use are untouched. use resolves them immediately, so they stay order-dependent (#2113, #1761).

Memory

The route endpoint gains a sixth ivar (the comment on build_prototype is updated). Per-request copies hold eight, and measured with ObjectSpace.memsize_of they stay at 80 bytes, with or without helpers.

Tests

Five examples under Grape::API .helpers:

  • a helper declared after a route of the same scope reaches it;
  • the same inside a namespace;
  • a redefinition answers every route of the scope, and warns, once, for the route in between;
  • no warning when the helper below only adds methods;
  • no warning for a redefinition in an enclosing scope.

Mutation-checked:

  • reading the helpers from the copy again fails the first three;
  • dropping the warning call fails the redefinition example;
  • warning for every method declared below the route fails the "only adds methods" example.

bundle exec rake: 378 files with no offenses, 3189 examples, 0 failures.

🤖 Generated with Claude Code

@ericproulx
ericproulx force-pushed the fix/helpers-declared-after-routes branch from a6c2d46 to 67f619d Compare November 7, 2026 13:11
@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx force-pushed the fix/helpers-declared-after-routes branch from 67f619d to dd6c2b4 Compare November 7, 2026 13:21
@ericproulx
ericproulx requested a review from dblock November 7, 2026 18:53
@ericproulx
ericproulx force-pushed the fix/helpers-declared-after-routes branch 2 times, most recently from 938849a to 8bb530f Compare November 8, 2026 20:15
An endpoint is built from a point-in-time copy of its scope's settings.
The copy keeps the scope's own helpers as they were at the route, but
links to the enclosing scopes live. So a `helpers` declared further
down the same scope never reached the route, and calling it raised
NameError. One declared further down an enclosing scope, or after
`mount`, did reach it.

The endpoint now also keeps the live settings of its scope and reads
the helpers from them when it compiles, by which time the whole class
body has run. A helper declared in a sibling namespace still doesn't
reach the route.

A helper defined twice in one scope therefore answers with its last
definition in every route of that scope, including those declared
between the two definitions. UPGRADING describes that change.

Since that change is silent, the endpoint warns about it when it
compiles, until 5.0. Its settings copy still holds the helpers the
route reached before, so it can tell exactly which methods a helper
declared further down now answers in their place. It warns only for
those: a helper that only adds methods doesn't warn, nor does a
redefinition in an enclosing scope, which already reached the route. The
warning lives in Endpoint::RedefinedHelpersWarning, to be deleted with
it in 5.0, which also keeps Endpoint under its ClassLength limit. The
README no longer lists `helpers` among the declarations that must come
above the routes they cover. `use`, `rescue_from` and the callbacks are
unchanged.

The route endpoint gains a sixth ivar. Per-request copies hold eight,
so their 80-byte slot is unchanged.

Fixes #34

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the fix/helpers-declared-after-routes branch from 8bb530f to b196dbe Compare November 8, 2026 20:31
@ericproulx
ericproulx merged commit adabadb into master Nov 8, 2026
52 checks passed
@ericproulx
ericproulx deleted the fix/helpers-declared-after-routes 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.

Helpers declared after routes aren't available.

2 participants