lib/grape/middleware/formatter.rb
# frozen_string_literal: true
require 'grape/middleware/base'
module Grape
module Middleware
class Formatter < Base
CHUNKED = 'chunked'
def default_options
{
default_format: :txt,
formatters: {},
parsers: {}
}
end
def before
negotiate_content_type
read_body_input
end
def after
return unless @app_response
status, headers, bodies = *@app_response
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
@app_response
else
build_formatted_response(status, headers, bodies)
end
end
private
def build_formatted_response(status, headers, bodies)
headers = ensure_content_type(headers)
if bodies.is_a?(Grape::ServeStream::StreamResponse)
Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp|
resp.body = bodies.stream
end
else
# Allow content-type to be explicitly overwritten
formatter = fetch_formatter(headers, options)
bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter: formatter, env: env) do
bodies.collect { |body| formatter.call(body, env) }
end
Rack::Response.new(bodymap, status, headers)
end
rescue Grape::Exceptions::InvalidFormatter => e
throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e
end
def fetch_formatter(headers, options)
api_format = mime_types[headers[Rack::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
Grape::Formatter.formatter_for(api_format, **options)
end
# Set the content type header for the API format if it is not already present.
#
# @param headers [Hash]
# @return [Hash]
def ensure_content_type(headers)
if headers[Rack::CONTENT_TYPE]
headers
else
headers.merge(Rack::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
end
end
def request
@request ||= Rack::Request.new(env)
end
# store read input in env['api.request.input']
def read_body_input
return unless
(request.post? || request.put? || request.patch? || request.delete?) &&
(!request.form_data? || !request.media_type) &&
!request.parseable_data? &&
(request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
return unless (input = env[Grape::Env::RACK_INPUT])
input.rewind
body = env[Grape::Env::API_REQUEST_INPUT] = input.read
begin
read_rack_input(body) if body && !body.empty?
ensure
input.rewind
end
end
# store parsed input in env['api.request.body']
def read_rack_input(body)
fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
parser = Grape::Parser.parser_for fmt, **options
if parser
begin
body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
if body.is_a?(Hash)
env[Grape::Env::RACK_REQUEST_FORM_HASH] = if env.key?(Grape::Env::RACK_REQUEST_FORM_HASH)
env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body)
else
body
end
env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT]
end
rescue Grape::Exceptions::Base => e
raise e
rescue StandardError => e
throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e
end
else
env[Grape::Env::API_REQUEST_BODY] = body
end
end
def negotiate_content_type
fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
if content_type_for(fmt)
env[Grape::Env::API_FORMAT] = fmt
else
throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
end
end
def format_from_extension
parts = request.path.split('.')
if parts.size > 1
extension = parts.last
# avoid symbol memory leak on an unknown format
return extension.to_sym if content_type_for(extension)
end
nil
end
def format_from_params
fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
# avoid symbol memory leak on an unknown format
return fmt.to_sym if content_type_for(fmt)
fmt
end
def format_from_header
mime_array.each do |t|
return mime_types[t] if mime_types.key?(t)
end
nil
end
def mime_array
accept = env[Grape::Http::Headers::HTTP_ACCEPT]
return [] unless accept
accept_into_mime_and_quality = %r{
(
\w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
(?:
(?:;[^,]*?)? # optionally multiple formats in a row
;\s*q=([\w.]+) # optional "quality" preference (eg q=0.5)
)?
}x
vendor_prefix_pattern = /vnd\.[^+]+\+/
accept.scan(accept_into_mime_and_quality)
.sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) }
.flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
end
end
end
end