Bringing Accept header parsing in line with the spec.

For reference:
  http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
  http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
This commit is contained in:
Pieter van de Bruggen 2013-01-28 09:43:30 -08:00
parent bac3f4f671
commit 43de6d2d8a
3 changed files with 62 additions and 8 deletions

View File

@ -16,12 +16,16 @@ module Sinatra
# The request object. See Rack::Request for more info:
# http://rack.rubyforge.org/doc/classes/Rack/Request.html
class Request < Rack::Request
HEADER_PARAM = /;[\d\w.]+=(?:[\d\w.]+|"(?:[^"\\]|\\.)*")?/
HEADER_VALUE_WITH_PARAMS = /(?:(?:\w+|\*)\/(?:\w+|\*))(?:#{HEADER_PARAM})*/
# Returns an array of acceptable media types for the response
def accept
@env['sinatra.accept'] ||= begin
entries = @env['HTTP_ACCEPT'].to_s.split(',')
entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
@env['sinatra.accept_entries'] ||= begin
entries = @env['HTTP_ACCEPT'].to_s.scan(HEADER_VALUE_WITH_PARAMS)
entries.map { |e| accept_entry(e) }.sort_by(&:last)
end
@env['sinatra.accept'] ||= @env['sinatra.accept_entries'].map(&:first)
end
def preferred_type(*types)
@ -51,10 +55,11 @@ module Sinatra
private
def accept_entry(entry)
type, *options = entry.delete(' ').split(';')
type = entry.delete(' ').split(';').first
options = entry.scan(HEADER_PARAM).map { |s| s[1..-1] }
quality = 0 # we sort smallest first
options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
[type, [quality, type.count('*'), 1 - options.size]]
[type, options, [quality, type.count('*'), 1 - options.size]]
end
end

View File

@ -431,7 +431,7 @@ class AfterFilterTest < Test::Unit::TestCase
get('/') { @type }
end
get('/', {}, { 'HTTP_ACCEPT' => '*' })
get('/', {}, { 'HTTP_ACCEPT' => '*/*' })
assert_body 'txt'
end
end

View File

@ -779,7 +779,7 @@ class RoutingTest < Test::Unit::TestCase
get('*', :provides => :html) { 'html' }
end
get '/', {}, { 'HTTP_ACCEPT' => '*' }
get '/', {}, { 'HTTP_ACCEPT' => '*/*' }
assert ok?
assert_equal 'text/plain;charset=utf-8', response.headers['Content-Type']
assert_body 'txt'
@ -826,7 +826,7 @@ class RoutingTest < Test::Unit::TestCase
assert_equal 'default', body
end
it 'respects user agent prefferences for the content type' do
it 'respects user agent preferences for the content type' do
mock_app { get('/', :provides => [:png, :html]) { content_type }}
get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5,text/html;q=0.8' }
assert_body 'text/html;charset=utf-8'
@ -881,6 +881,55 @@ class RoutingTest < Test::Unit::TestCase
assert_body 'text/html;charset=utf-8'
end
it 'supplies a default quality of 1.0' do
mock_app { get('/', :provides => [:png, :html]) { content_type }}
get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5, text/*' }
assert_body 'text/html;charset=utf-8'
end
it 'orders types with equal quality by parameter count' do
mock_app do
get('/', :provides => [:png, :jpg]) { content_type }
end
lo_png = 'image/png;q=0.5'
hi_png = 'image/png;q=0.5;profile=FOGRA40;gamma=0.8'
jpeg = 'image/jpeg;q=0.5;compress=0.25'
get '/', {}, { 'HTTP_ACCEPT' => "#{lo_png}, #{jpeg}" }
assert_body 'image/jpeg'
get '/', {}, { 'HTTP_ACCEPT' => "#{hi_png}, #{jpeg}" }
assert_body 'image/png'
end
it 'ignores the quality parameter when ordering by parameter count' do
mock_app do
get('/', :provides => [:png, :jpg]) { content_type }
end
lo_png = 'image/png'
hi_png = 'image/png;profile=FOGRA40;gamma=0.8'
jpeg = 'image/jpeg;q=1.0;compress=0.25'
get '/', {}, { 'HTTP_ACCEPT' => "#{jpeg}, #{lo_png}" }
assert_body 'image/jpeg'
get '/', {}, { 'HTTP_ACCEPT' => "#{jpeg}, #{hi_png}" }
assert_body 'image/png'
end
it 'properly handles quoted strings in parameters' do
mock_app do
get('/', :provides => [:png, :jpg]) { content_type }
end
get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5;profile=",image/jpeg,"' }
assert_body 'image/png'
get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5,image/jpeg;q=0;x=";q=1.0"' }
assert_body 'image/png'
get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5,image/jpeg;q=0;x="\";q=1.0"' }
assert_body 'image/png'
end
it 'accepts both text/javascript and application/javascript for js' do
mock_app { get('/', :provides => :js) { content_type }}
get '/', {}, { 'HTTP_ACCEPT' => 'application/javascript' }