Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(#1922): Allow to use instance variables defined in the endpoints inside rescue_from #2377

Merged
merged 6 commits into from
Nov 24, 2023
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
Upgrading Grape
===============

### Upgrading to >= 2.0.1
### Upgrading to >= 2.1.0

#### Grape::Router::Route.route_xxx methods have been removed

- `route_method` is accessible through `request_method`
- `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`. This means the scope of the instance variables has changed.

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.
jcagarcia marked this conversation as resolved.
Show resolved Hide resolved

### Upgrading to >= 2.0.0

#### Headers
Expand Down
17 changes: 17 additions & 0 deletions lib/grape/dsl/inside_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
dblock marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
16 changes: 12 additions & 4 deletions lib/grape/middleware/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
dblock marked this conversation as resolved.
Show resolved Hide resolved

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
57 changes: 57 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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