diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2f2045f..44c056462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/lib/grape/router.rb b/lib/grape/router.rb index f72d0fda0..944ce20db 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -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 diff --git a/spec/grape/router_spec.rb b/spec/grape/router_spec.rb index 2a864791b..84cffdbad 100644 --- a/spec/grape/router_spec.rb +++ b/spec/grape/router_spec.rb @@ -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