Permalink
Browse files

Fix the parsing and evaluation of the Accepts Header (#229)

  • Loading branch information...
mereghost authored and jodosha committed Jul 10, 2017
1 parent 27b3e19 commit 20441e2dbdab1bf0c47b062c3ffea4473ef60953
@@ -265,13 +265,8 @@ def format
# end
def content_type
return @content_type unless @content_type.nil?

if accept_header?
type = content_type_from_accept_header
return type if type
end

default_response_type || default_content_type || DEFAULT_CONTENT_TYPE
@content_type = content_type_from_accept_header if accept_header?
@content_type || default_response_type || default_content_type || DEFAULT_CONTENT_TYPE
end

# Action charset setter, receives new charset value
@@ -565,33 +560,61 @@ def content_type_with_charset
# @see https://github.com/rack/rack/pull/659
# @see https://github.com/hanami/controller/issues/59
# @see https://github.com/hanami/controller/issues/104
# @see https://github.com/hanami/controller/issues/275
def best_q_match(q_value_header, available_mimes)
values = ::Rack::Utils.q_values(q_value_header)
values = values.map do |req_mime, quality|
if req_mime == DEFAULT_ACCEPT
# See https://github.com/hanami/controller/issues/167
match = default_content_type
else
match = available_mimes.find { |am| ::Rack::Mime.match?(am, req_mime) }
end
::Rack::Utils.q_values(q_value_header).each_with_index.map do |(req_mime, quality), index|
match = available_mimes.find { |am| ::Rack::Mime.match?(am, req_mime) }
next unless match
[match, quality]
end.compact

if Hanami::Utils.jruby?
# See https://github.com/hanami/controller/issues/59
# See https://github.com/hanami/controller/issues/104
values.reverse!
else
# See https://github.com/jruby/jruby/issues/3004
values.sort!
RequestMimeWeight.new(req_mime, quality, index, match)
end.compact.max&.format
end

# @since 1.0.1
# @api private
class RequestMimeWeight
include Comparable

# @since 1.0.1
# @api private
attr_reader :quality

# @since 1.0.1
# @api private
attr_reader :index

# @since 1.0.1
# @api private
attr_reader :mime

# @since 1.0.1
# @api private
attr_reader :format

# @since 1.0.1
# @api private
attr_reader :priority

# @since 1.0.1
# @api private
def initialize(mime, quality, index, format = mime)
@quality, @index, @format = quality, index, format
calculate_priority(mime)
end

value = values.sort_by do |match, quality|
(match.split('/'.freeze, 2).count('*'.freeze) * -10) + quality
end.last
# @since 1.0.1
# @api private
def <=>(other)
return priority <=> other.priority unless priority == other.priority
other.index <=> index
end

private

value.first if value
# @since 1.0.1
# @api private
def calculate_priority(mime)
@priority ||= (mime.split('/'.freeze, 2).count('*'.freeze) * -10) + quality
end
end
end
end
@@ -11,6 +11,7 @@
get '/response', to: 'mimes#default_response'
get '/overwritten_format', to: 'mimes#override_default_response'
get '/custom_from_accept', to: 'mimes#custom_from_accept'
get '/default_and_accept', to: 'mimes#default_and_accept'
end

module Mimes
@@ -115,6 +116,16 @@ def call(_params)
self.format = :xml
end
end

class DefaultAndAccept
include Hanami::Action
configuration.default_request_format :html
accept :json

def call(params)
self.body = format.to_s
end
end
end

RSpec.describe 'MIME Type' do
@@ -254,6 +265,18 @@ def call(_params)
expect(response.body).to eq("html")
end
end

context 'applies the weighting mechanism for media ranges' do
let(:accept) { "text/*,application/json,text/html,*/*" }

it "accepts selected mime types" do
expect(response.headers["X-AcceptDefault"]).to eq("true")
expect(response.headers["X-AcceptHtml"]).to eq("true")
expect(response.headers["X-AcceptXml"]).to eq("true")
expect(response.headers["X-AcceptJson"]).to eq("true")
expect(response.body).to eq("json")
end
end
end
end

@@ -324,6 +347,17 @@ def call(_params)
expect(response.body).to eq("html")
end
end

# See https://github.com/hanami/controller/issues/225
context "with an accepted format and default request format" do
let(:accept) { "text/*,application/json,text/html,*/*" }
let(:response) { app.get("/restricted", "HTTP_ACCEPT" => accept) }

it "defaults to the accepted format" do
expect(response.status).to be(200)
expect(response.body).to eq('json')
end
end
end
end
end
@@ -8,6 +8,17 @@ def call(params)
end
end

class JsonLookup
include Hanami::Action
configuration.handle_exceptions = false
configuration.default_request_format :html
accept :json

def call(params)
end
end


class Custom
include Hanami::Action
configuration.handle_exceptions = false
@@ -115,6 +126,15 @@ def call(_params)
end
end

# See https://github.com/hanami/controller/issues/225
it "accepts 'application/json, text/plain, */*' and returns :json" do
action = FormatController::JsonLookup.new
status, headers, _ = action.call('HTTP_ACCEPT' => 'application/json,text/plain,*/*')
expect(action.format).to eq(:json)
expect(headers['Content-Type']).to eq('application/json; charset=utf-8')
expect(status).to eq(200)
end

Hanami::Action::Mime::MIME_TYPES.each do |format, mime_type|
it "sets #{format} and returns '#{mime_type}'" do
_, headers, = action.call(format: format)
@@ -0,0 +1,48 @@
require 'spec_helper'

RSpec.describe Hanami::Action::Mime::RequestMimeWeight do
let(:plain_text) { described_class.new('text/plain', 0.7, 2) }
let(:any_text) { described_class.new('text/*', 1, 0) }
let(:anything) { described_class.new('*/*', 1, 3) }

it 'compares against another Specification' do
expect(described_class.new('text/plain', 1, 2)).to be > plain_text
expect(plain_text).to be > any_text
expect(anything).to be < plain_text
expect(described_class.new('text/*', 0.8, 0)).to be < any_text

list = [plain_text, anything, any_text]
expect(list.sort).to eq([anything, any_text, plain_text])
end

context '#priority' do
it 'returns a lower priority for media ranges' do
expect(plain_text.priority).to eq(0.7)
expect(any_text.priority).to eq(-9)
expect(anything.priority).to eq(-19)
end

it 'applies the quality of the mime type' do
low_quality = described_class.new('text/plain', 0.2, 0)
expect(low_quality.priority).to eq(0.2)

high_quality_media_range = described_class.new('text/*', 0.8, 0)
expect(high_quality_media_range.priority).to eq(-9.2)
end
end

context '#<=>' do
let(:html) { described_class.new('text/html', 1, 4) }
let(:json) { described_class.new('application/json', 1, 1) }


it 'checks priority first' do
expect(anything <=> json).to eq(-1)
expect(any_text <=> anything).to eq(1)
end

it 'against same priority and quality, a lower index takes precedence' do
expect(html <=> json).to eq(-1)
end
end
end

0 comments on commit 20441e2

Please sign in to comment.