Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
* [#2825](https://github.com/ruby-grape/grape/pull/2825): Stop `present` entity autodetection from picking up a top-level `::Entity` constant - [@ericproulx](https://github.com/ericproulx).
* [#2827](https://github.com/ruby-grape/grape/pull/2827): Make the `cascade` DSL getter return the configured value (`cascade false` read back as `true`) - [@ericproulx](https://github.com/ericproulx).
* [#2829](https://github.com/ruby-grape/grape/pull/2829): Fix a cascading route handing over only to the last route registered for the path, making a middle version (3+ mounted versions with a catch-all) answer 406 - [@ericproulx](https://github.com/ericproulx).
* [#2834](https://github.com/ruby-grape/grape/pull/2834): Restore the #2824 fix for cascaded routes leaking `route_info` and path captures, silently reverted by #2829 - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

### 3.3.4 (2026-07-25)
Expand Down
8 changes: 6 additions & 2 deletions lib/grape/router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,14 @@ def close_body(response)
body.close if body.respond_to?(:close)
end

# Routing args are rebuilt for every attempt: when a route cascades
# (X-Cascade pass), the next candidate must not observe the previous
# attempt's +route_info+ or path captures.
def process_route(route, input, env, include_allow_header: false)
route_params = route.params_for(input)
env[Grape::Env::GRAPE_ROUTING_ARGS] ||= { route_info: route }
env[Grape::Env::GRAPE_ROUTING_ARGS].merge!(route_params) if route_params.present?
routing_args = { route_info: route }
routing_args.merge!(route_params) if route_params.present?
env[Grape::Env::GRAPE_ROUTING_ARGS] = routing_args
env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header
route.call(env)
end
Expand Down
35 changes: 35 additions & 0 deletions spec/grape/router_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,39 @@ def response_body
expect(headers['X-Cascade']).to eq('pass')
end
end

# Regression: routing args were seeded once (`||=`) and merged in place, so
# when a route cascaded (X-Cascade pass) the next candidate still saw the
# previous attempt's :route_info and path captures.
describe 'routing args across cascading routes' do
let(:app) do
v2 = Class.new(Grape::API) do
version 'v2', using: :header, vendor: 'grape', cascade: true
get ':id' do
{ from: 'v2' }
end
end
v1 = Class.new(Grape::API) do
format :json
version 'v1', using: :header, vendor: 'grape'
get ':name' do
{ origin: route.origin, params: params.to_h }
end
end
Class.new(Grape::API) do
format :json
mount v2
mount v1
end
end

it 'does not leak route_info or path captures from a cascaded attempt' do
get '/123', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-v1+json'

expect(last_response.status).to eq(200)
body = JSON.parse(last_response.body)
expect(body['origin']).to eq('/:name')
expect(body['params']).to eq('name' => '123')
end
end
end
Loading