From 103928adc7fa09846011a9e00b4e716f7dea0465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Carlos=20Garc=C3=ADa=20del=20Canto?= Date: Fri, 24 Nov 2023 15:50:26 +0100 Subject: [PATCH] fix(#1922): Allow to use instance variables defined in the endpoints inside rescue_from (#2377) * fix(#1922): Allow to use instance variables defined in the endpoints when rescue_from * fix(#1922): Update CHANGELOG and running rubocop * fix(#1922): Updating UPGRADING and README files explaining the instance variables behavior. Extra tests added * fix(#1922): Send endpoint parameter as the last param of the run_rescue_handler method * fix(#1922): Adding short before/after example to UPGRADING * fix(#1922): Fixing CHANGELOG format style --- CHANGELOG.md | 1 + README.md | 37 +++++++++++++++++++++++ UPGRADING.md | 44 ++++++++++++++++++++++++++- lib/grape/dsl/inside_route.rb | 17 +++++++++++ lib/grape/middleware/error.rb | 16 +++++++--- spec/grape/api_spec.rb | 57 +++++++++++++++++++++++++++++++++++ 6 files changed, 167 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40acedb2af..c1fa986c24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia). +* [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index e6b68bc9d3..f599d5bc44 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ - [Current Route and Endpoint](#current-route-and-endpoint) - [Before, After and Finally](#before-after-and-finally) - [Anchoring](#anchoring) +- [Instance Variables](#instance-variables) - [Using Custom Middleware](#using-custom-middleware) - [Grape Middleware](#grape-middleware) - [Rails Middleware](#rails-middleware) @@ -3595,6 +3596,42 @@ end This will match all paths starting with '/statuses/'. There is one caveat though: the `params[:status]` parameter only holds the first part of the request url. Luckily this can be circumvented by using the described above syntax for path specification and using the `PATH_INFO` Rack environment variable, using `env['PATH_INFO']`. This will hold everything that comes after the '/statuses/' part. +## Instance Variables + +You can use instance variables to pass information across the various stages of a request. An instance variable set within a `before` validator is accessible within the endpoint's code and can also be utilized within the `rescue_from` handler. + +```ruby +class TwitterAPI < Grape::API + before do + @var = 1 + end + + get '/' do + puts @var # => 1 + raise + end + + rescue_from :all do + puts @var # => 1 + end +end +``` + +The values of instance variables cannot be shared among various endpoints within the same API. This limitation arises due to Grape generating a new instance for each request made. Consequently, instance variables set within an endpoint during one request differ from those set during a subsequent request, as they exist within separate instances. + +```ruby +class TwitterAPI < Grape::API + get '/first' do + @var = 1 + puts @var # => 1 + end + + get '/second' do + puts @var # => nil + end +end +``` + ## Using Custom Middleware ### Grape Middleware diff --git a/UPGRADING.md b/UPGRADING.md index 4cb1d4c62a..fb53565d07 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,7 +1,7 @@ Upgrading Grape =============== -### Upgrading to >= 2.0.1 +### Upgrading to >= 2.1.0 #### Grape::Router::Route.route_xxx methods have been removed @@ -9,6 +9,48 @@ Upgrading Grape - `route_path` is accessible through `path` - Any other `route_xyz` are accessible through `options[xyz]` +#### Instance variables scope + +Due to the changes done in [#2377](https://github.com/ruby-grape/grape/pull/2377), the instance variables defined inside each of the endpoints (or inside a `before` validator) are now accessible inside the `rescue_from`. The behavior of the instance variables was undefined until `2.1.0`. + +If you were using the same variable name defined inside an endpoint or `before` validator inside a `rescue_from` handler, you need to take in mind that you can start getting different values or you can be overriding values. + +Before: +```ruby +class TwitterAPI < Grape::API + before do + @var = 1 + end + + get '/' do + puts @var # => 1 + raise + end + + rescue_from :all do + puts @var # => nil + end +end +``` + +After: +```ruby +class TwitterAPI < Grape::API + before do + @var = 1 + end + + get '/' do + puts @var # => 1 + raise + end + + rescue_from :all do + puts @var # => 1 + end +end +``` + ### Upgrading to >= 2.0.0 #### Headers diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 7ebd6aab8f..0286595b8c 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -167,6 +167,23 @@ def error!(message, status = nil, additional_headers = nil) throw :error, message: message, status: self.status, headers: headers end + # Creates a Rack response based on the provided message, status, and headers. + # The content type in the headers is set to the default content type unless provided. + # The message is HTML-escaped if the content type is 'text/html'. + # + # @param message [String] The content of the response. + # @param status [Integer] The HTTP status code. + # @params headers [Hash] (optional) Headers for the response + # (default: {Rack::CONTENT_TYPE => content_type}). + # + # Returns: + # A Rack::Response object containing the specified message, status, and headers. + # + def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => content_type }) + message = ERB::Util.html_escape(message) if headers[Rack::CONTENT_TYPE] == 'text/html' + Rack::Response.new([message], Rack::Utils.status_code(status), headers) + end + # Redirect to a new url. # # @param url [String] The url to be redirect. diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 05fc0312f2..02db9aae0d 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -46,7 +46,7 @@ def call!(env) rescue_handler_for_any_class(e.class) || raise - run_rescue_handler(handler, e) + run_rescue_handler(handler, e, @env[Grape::Env::API_ENDPOINT]) end end @@ -119,21 +119,29 @@ def rescue_handler_for_any_class(klass) options[:all_rescue_handler] || :default_rescue_handler end - def run_rescue_handler(handler, error) + def run_rescue_handler(handler, error, endpoint) if handler.instance_of?(Symbol) raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler) handler = public_method(handler) end - response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler) + response = (catch(:error) do + handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler) + end) + + response = error!(response[:message], response[:status], response[:headers]) if error?(response) if response.is_a?(Rack::Response) response else - run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new) + run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new, endpoint) end end + + def error?(response) + response.is_a?(Hash) && response[:message] && response[:status] && response[:headers] + end end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index b67dcbe24e..f2ca6d55d7 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -4352,4 +4352,61 @@ def uniqe_id_route expect(last_response.body).to be_eql('1-2') end end + + context 'instance variables' do + context 'when setting instance variables in a before validation' do + it 'is accessible inside the endpoint' do + expected_instance_variable_value = 'wadus' + + subject.before do + @my_var = expected_instance_variable_value + end + + subject.get('/') do + { my_var: @my_var }.to_json + end + + get '/' + expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) + end + end + + context 'when setting instance variables inside the endpoint code' do + it 'is accessible inside the rescue_from handler' do + expected_instance_variable_value = 'wadus' + + subject.rescue_from(:all) do + body = { my_var: @my_var } + error!(body, 400) + end + + subject.get('/') do + @my_var = expected_instance_variable_value + raise + end + + get '/' + expect(last_response.status).to be 400 + expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) + end + + it 'is NOT available in other endpoints of the same api' do + expected_instance_variable_value = 'wadus' + + subject.get('/first') do + @my_var = expected_instance_variable_value + { my_var: @my_var }.to_json + end + + subject.get('/second') do + { my_var: @my_var }.to_json + end + + get '/first' + expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) + get '/second' + expect(last_response.body).to eq({ my_var: nil }.to_json) + end + end + end end