1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
|
# 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[Grape::Http::Headers::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[Grape::Http::Headers::CONTENT_TYPE]
headers
else
headers.merge(Grape::Http::Headers::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[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=([\d.]+) # 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.to_f }
.flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
end
end
end
end
|