Skip to content
Draft
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 @@ -16,6 +16,7 @@
* [#2692](https://github.com/ruby-grape/grape/pull/2692): Replace per-request `Proc` allocation in `Router#transaction` with a `halt?` helper - [@ericproulx](https://github.com/ericproulx).
* [#2694](https://github.com/ruby-grape/grape/pull/2694): Split `Versioner::Base#available_media_types` into an `attr_reader` plus `build_available_media_types` - [@ericproulx](https://github.com/ericproulx).
* [#2695](https://github.com/ruby-grape/grape/pull/2695): Lift trailing `if/else` into guard clauses - [@ericproulx](https://github.com/ericproulx).
* [#2696](https://github.com/ruby-grape/grape/pull/2696): Reduce per-request allocations on the request hot path - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
10 changes: 5 additions & 5 deletions lib/grape/api/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,18 +175,18 @@ def collect_route_config_per_pattern(all_routes)
end
end

ROOT_PREFIX_VERSIONING_KEY = %i[version version_options root_prefix].freeze
private_constant :ROOT_PREFIX_VERSIONING_KEY
ROOT_PREFIX_VERSIONING_KEYS = %i[version version_options root_prefix].freeze
private_constant :ROOT_PREFIX_VERSIONING_KEYS

# Allows definition of endpoints that ignore the versioning configuration
# used by the rest of your API.
def without_root_prefix_and_versioning
inheritable_setting = self.class.inheritable_setting
deleted_values = inheritable_setting.namespace_inheritable.delete(*ROOT_PREFIX_VERSIONING_KEY)
deleted_values = inheritable_setting.namespace_inheritable.delete(*ROOT_PREFIX_VERSIONING_KEYS)
yield
ensure
ROOT_PREFIX_VERSIONING_KEY.each_with_index do |key, index|
inheritable_setting.namespace_inheritable[key] = deleted_values[index]
ROOT_PREFIX_VERSIONING_KEYS.zip(deleted_values) do |key, value|
inheritable_setting.namespace_inheritable[key] = value
end
end
end
Expand Down
16 changes: 9 additions & 7 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def inspect

def run
ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env:) do
@request = Grape::Request.new(env, build_params_with: inheritable_setting.namespace_inheritable[:build_params_with])
@request = Grape::Request.new(env, build_params_with: @build_params_with)
begin
run_filters befores, :before
@before_filter_passed = true
Expand All @@ -150,7 +150,6 @@ def run
header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
raise Grape::Exceptions::MethodNotAllowed.new(header) unless options?

header 'Allow', header['Allow']
response_object = ''
status 204
else
Expand Down Expand Up @@ -215,11 +214,7 @@ def run_filters(filters, type = :other)
end
end

%i[befores before_validations after_validations afters finallies].each do |method|
define_method method do
inheritable_setting.namespace_stackable[method]
end
end
attr_reader :befores, :before_validations, :after_validations, :afters, :finallies

def options?
options[:options_route_enabled] &&
Expand All @@ -233,6 +228,13 @@ def options?
def compile!
@app = options[:app] || build_stack
@helpers = build_helpers
stackable = inheritable_setting.namespace_stackable
@befores = stackable[:befores]
@before_validations = stackable[:before_validations]
@after_validations = stackable[:after_validations]
@afters = stackable[:afters]
@finallies = stackable[:finallies]
@build_params_with = inheritable_setting.namespace_inheritable[:build_params_with]
end

def to_routes
Expand Down
4 changes: 2 additions & 2 deletions lib/grape/error_formatter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module ErrorFormatter
class Base
class << self
def call(message, backtrace, options = {}, env = nil, original_exception = nil)
merge_backtrace = backtrace.present? && options.dig(:rescue_options, :backtrace)
merge_original_exception = original_exception && options.dig(:rescue_options, :original_exception)
merge_backtrace = backtrace.present? && options&.dig(:backtrace)
merge_original_exception = original_exception && options&.dig(:original_exception)

wrapped_message = wrap_message(present(message, env))
if wrapped_message.is_a?(Hash)
Expand Down
2 changes: 1 addition & 1 deletion lib/grape/middleware/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Base
# @param [Hash] options A hash of options, simply stored for use by subclasses.
def initialize(app, **options)
@app = app
@options = merge_default_options(options)
@options = merge_default_options(options).freeze
@app_response = nil
end

Expand Down
60 changes: 43 additions & 17 deletions lib/grape/middleware/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,44 @@ class Error < Base
include PrecomputedContentTypes

DEFAULT_OPTIONS = {
default_status: 500,
all_rescue_handler: nil,
base_only_rescue_handlers: nil,
default_error_formatter: nil,
default_message: '',
default_status: 500,
error_formatters: nil,
format: :txt,
grape_exceptions_rescue_handler: nil,
rescue_all: false,
rescue_grape_exceptions: false,
rescue_subclasses: true,
rescue_handlers: nil,
rescue_options: {
backtrace: false,
original_exception: false
}.freeze
}.freeze

attr_reader :all_rescue_handler, :base_only_rescue_handlers, :default_error_formatter,
:default_message, :default_status, :error_formatters, :format,
:grape_exceptions_rescue_handler, :rescue_all, :rescue_grape_exceptions,
:rescue_handlers, :rescue_options

def initialize(app, **options)
super
@all_rescue_handler = @options[:all_rescue_handler]
@base_only_rescue_handlers = @options[:base_only_rescue_handlers]
@default_error_formatter = @options[:default_error_formatter]
@default_message = @options[:default_message]
@default_status = @options[:default_status]
@error_formatters = @options[:error_formatters]
@format = @options[:format]
@grape_exceptions_rescue_handler = @options[:grape_exceptions_rescue_handler]
@rescue_all = @options[:rescue_all]
@rescue_grape_exceptions = @options[:rescue_grape_exceptions]
@rescue_handlers = @options[:rescue_handlers]
@rescue_options = @options[:rescue_options]
end

def call!(env)
@env = env
error_response(catch(:error) { return @app.call(@env) })
Expand All @@ -33,13 +59,13 @@ def rack_response(status, headers, message)
end

def format_message(message, backtrace, original_exception = nil)
format = env[Grape::Env::API_FORMAT] || options[:format]
formatter = Grape::ErrorFormatter.formatter_for(format, options[:error_formatters], options[:default_error_formatter])
return formatter.call(message, backtrace, options, env, original_exception) if formatter
current_format = env[Grape::Env::API_FORMAT] || format
formatter = Grape::ErrorFormatter.formatter_for(current_format, error_formatters, default_error_formatter)
return formatter.call(message, backtrace, rescue_options, env, original_exception) if formatter

throw :error,
status: 406,
message: "The requested format '#{format}' is not supported.",
message: "The requested format '#{current_format}' is not supported.",
backtrace:,
original_exception:
end
Expand All @@ -52,9 +78,9 @@ def find_handler(klass)
end

def error_response(error = {})
status = error[:status] || options[:default_status]
status = error[:status] || default_status
env[Grape::Env::API_ENDPOINT].status(status) # error! may not have been called
message = error[:message] || options[:default_message]
message = error[:message] || default_message
headers = { Rack::CONTENT_TYPE => content_type }.tap do |h|
h.merge!(error[:headers]) if error[:headers].is_a?(Hash)
end
Expand All @@ -68,12 +94,12 @@ def default_rescue_handler(exception)
end

def registered_rescue_handler(klass)
rescue_handler_from(:base_only_rescue_handlers) { |err| klass == err } ||
rescue_handler_from(:rescue_handlers) { |err| klass <= err }
rescue_handler_from(base_only_rescue_handlers) { |err| klass == err } ||
rescue_handler_from(rescue_handlers) { |err| klass <= err }
end

def rescue_handler_from(key)
error, handler = options[key]&.find { |err, _handler| yield(err) }
def rescue_handler_from(handlers)
error, handler = handlers&.find { |err, _handler| yield(err) }

return unless error

Expand All @@ -83,16 +109,16 @@ def rescue_handler_from(key)
def rescue_handler_for_grape_exception(klass)
return unless klass <= Grape::Exceptions::Base
return method(:error_response) if klass == Grape::Exceptions::InvalidVersionHeader
return unless options[:rescue_grape_exceptions] || !options[:rescue_all]
return unless rescue_grape_exceptions || !rescue_all

options[:grape_exceptions_rescue_handler] || method(:error_response)
grape_exceptions_rescue_handler || method(:error_response)
end

def rescue_handler_for_any_class(klass)
return unless klass <= StandardError
return unless options[:rescue_all] || options[:rescue_grape_exceptions]
return unless rescue_all || rescue_grape_exceptions

options[:all_rescue_handler] || method(:default_rescue_handler)
all_rescue_handler || method(:default_rescue_handler)
end

def run_rescue_handler(handler, error, endpoint)
Expand All @@ -110,7 +136,7 @@ def run_rescue_handler(handler, error, endpoint)
end
end

def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
def error!(message, status = default_status, headers = {}, backtrace = [], original_exception = nil)
env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route
rack_response(
status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type),
Expand Down
28 changes: 21 additions & 7 deletions lib/grape/middleware/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,25 @@ class Formatter < Base
include PrecomputedContentTypes

DEFAULT_OPTIONS = {
default_format: :txt
content_types: nil,
default_format: :txt,
format: nil,
formatters: nil,
parsers: nil
}.freeze

ALL_MEDIA_TYPES = '*/*'

attr_reader :default_format, :format, :formatters, :parsers

def initialize(app, **options)
super
@default_format = @options[:default_format]
@format = @options[:format]
@formatters = @options[:formatters]
@parsers = @options[:parsers]
end

def before
negotiate_content_type
read_body_input
Expand Down Expand Up @@ -39,7 +53,7 @@ def build_formatted_response(status, headers, bodies)
end
else
# Allow content-type to be explicitly overwritten
formatter = fetch_formatter(headers, options)
formatter = fetch_formatter(headers)
bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter:, env:) do
bodies.map { |body| formatter.call(body, env) }
end
Expand All @@ -49,9 +63,9 @@ def build_formatted_response(status, headers, bodies)
throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e
end

def fetch_formatter(headers, options)
def fetch_formatter(headers)
api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
Grape::Formatter.formatter_for(api_format, options[:formatters])
Grape::Formatter.formatter_for(api_format, formatters)
end

# Set the content type header for the API format if it is not already present.
Expand Down Expand Up @@ -84,10 +98,10 @@ def read_rack_input(body)
return if body.empty?

media_type = rack_request.media_type
fmt = media_type ? mime_types[media_type] : options[:default_format]
fmt = media_type ? mime_types[media_type] : default_format

throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt)
parser = Grape::Parser.parser_for fmt, options[:parsers]
parser = Grape::Parser.parser_for fmt, parsers
return env[Grape::Env::API_REQUEST_BODY] = body unless parser

begin
Expand Down Expand Up @@ -121,7 +135,7 @@ def read_body_input?
end

def negotiate_content_type
fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format]
fmt = format_from_extension || query_params['format'] || format || format_from_header || default_format
if content_type_for(fmt)
env[Grape::Env::API_FORMAT] = fmt.to_sym
else
Expand Down
31 changes: 14 additions & 17 deletions lib/grape/middleware/versioner/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,39 @@ class Base < Grape::Middleware::Base
include Grape::Middleware::PrecomputedContentTypes

DEFAULT_OPTIONS = {
mount_path: nil,
pattern: /.*/i,
prefix: nil,
mount_path: nil,
version_options: {
strict: false,
cascade: true,
parameter: 'apiver',
strict: false,
vendor: nil
}.freeze
}.freeze

CASCADE_PASS_HEADER = { 'X-Cascade' => 'pass' }.freeze

DEFAULT_OPTIONS.each_key do |key|
define_method key do
options[key]
end
end

DEFAULT_OPTIONS[:version_options].each_key do |key|
define_method key do
options[:version_options][key]
end
end

def self.inherited(klass)
super
Versioner.register(klass)
end

attr_reader :error_headers, :versions, :available_media_types
attr_reader :available_media_types, :cascade, :error_headers, :mount_path, :parameter,
:pattern, :prefix, :strict, :vendor, :versions

def initialize(app, **options)
super
@error_headers = cascade ? CASCADE_PASS_HEADER : {}
@versions = options[:versions]&.map(&:to_s) # making sure versions are strings to ease potential match
version_options = @options[:version_options]
@cascade = version_options[:cascade]
@mount_path = @options[:mount_path]
@parameter = version_options[:parameter]
@pattern = @options[:pattern]
@prefix = @options[:prefix]
@strict = version_options[:strict]
@vendor = version_options[:vendor]
@versions = @options[:versions]&.map(&:to_s) # making sure versions are strings to ease potential match
@error_headers = @cascade ? CASCADE_PASS_HEADER : {}
@available_media_types = build_available_media_types
end

Expand Down
6 changes: 4 additions & 2 deletions lib/grape/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,10 @@ def cookies

def make_params
params = @params_builder.call(rack_params)
routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]&.except(:version, :route_info)&.presence
routing_args ? params.deep_merge!(routing_args) : params
routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
return params unless routing_args&.any? { |k, _| k != :version && k != :route_info }

params.deep_merge!(routing_args.except(:version, :route_info))
rescue *Grape::RACK_ERRORS
raise Grape::Exceptions::RequestError
end
Expand Down
2 changes: 1 addition & 1 deletion lib/grape/util/inheritable_values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Grape
module Util
class InheritableValues < BaseInheritable
def [](name)
values[name]
@new_values.fetch(name) { @inherited_values[name] }
end

def []=(name, value)
Expand Down
15 changes: 4 additions & 11 deletions lib/grape/validations/types/custom_type_coercer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,10 @@ def coerced?(val)
# @param method see #new
# @return [#call] coercion method
def infer_coercion_method(type, method)
if method
if method.respond_to? :parse
method.method :parse
else
method
end
else
# Try to use parse() declared on the target type.
# This may raise an exception, but we are out of ideas anyway.
type.method :parse
end
return type.method(:parse) unless method
return method unless method.respond_to?(:parse)

method.method(:parse)
end

# Determine how the type validity of a coerced
Expand Down
Loading
Loading