Permalink
Browse files

Improve handling of Accept header:

* allow wild cards
* respect preferences
* treat both text/xml and application/xml as xml
* treat both text/javascript and application/javascript as js

This impacts mainly the `provides` condition, but should improve the behavior
of any code using Request#accept and or Request#preferred_type.

Tests included.
Fixes #230.
  • Loading branch information...
1 parent e97c3fb commit 9808369bb96fe05f4073dcdc77688433738a0be9 @rkh rkh committed Mar 25, 2011
Showing with 107 additions and 7 deletions.
  1. +36 −7 lib/sinatra/base.rb
  2. +71 −0 test/routing_test.rb
View
@@ -14,7 +14,19 @@ module Sinatra
class Request < Rack::Request
# Returns an array of acceptable media types for the response
def accept
- @env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.split(';')[0].strip }
+ @env['sinatra.accept'] ||= begin
+ entries = @env['HTTP_ACCEPT'].to_s.split(',')
+ entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
+ end
+ end
+
+ def preferred_type(*types)
+ return accept.first if types.empty?
+ types.flatten!
+ accept.detect do |pattern|
+ type = types.detect { |t| File.fnmatch(pattern, t) }
+ return type if type
+ end
end
if Rack.release <= "1.2"
@@ -44,6 +56,15 @@ def path_info=(value)
@route = nil
super
end
+
+ private
+
+ def accept_entry(entry)
+ type, *options = entry.gsub(/\s/, '').split(';')
+ quality = 0 # we sort smalles first
+ options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
+ [type, [quality, type.count('*'), 1 - options.size]]
+ end
end
# The response object. See Rack::Response and Rack::ResponseHelpers for
@@ -156,7 +177,8 @@ def mime_type(type)
# Set the Content-Type of the response body given a media type or file
# extension.
- def content_type(type, params={})
+ def content_type(type = nil, params={})
+ return response['Content-Type'] unless type
default = params.delete :default
mime_type = mime_type(type) || default
fail "Unknown media type: %p" % type if mime_type.nil?
@@ -1013,6 +1035,14 @@ def mime_type(type, value=nil)
Rack::Mime::MIME_TYPES[type] = value
end
+ # provides all mime types matching type, including deprecated types:
+ # mime_types :html # => ['text/html']
+ # mime_types :js # => ['application/javascript', 'text/javascript']
+ def mime_types(type)
+ type = mime_type type
+ type =~ /^application\/(xml|javascript)$/ ? [type, "text/#$1"] : [type]
+ end
+
# Define a before filter; runs before all requests within the same
# context as route handlers and may access/modify the request and
# response.
@@ -1065,12 +1095,11 @@ def user_agent(pattern)
# Condition for matching mimetypes. Accepts file extensions.
def provides(*types)
- types.map! { |t| mime_type(t) }
-
+ types.map! { |t| mime_types(t) }
+ types.flatten!
condition do
- matching_types = (request.accept & types)
- unless matching_types.empty?
- content_type matching_types.first
+ if type = request.preferred_type(types)
+ content_type(type)
true
else
false
View
@@ -724,6 +724,77 @@ class RoutingTest < Test::Unit::TestCase
assert_equal 'default', body
end
+ it 'respects user agent prefferences 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'
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.8,text/html;q=0.5' }
+ assert_body 'image/png'
+ end
+
+ it 'accepts generic types' do
+ mock_app do
+ get('/', :provides => :xml) { content_type }
+ get('/') { 'no match' }
+ end
+ get '/'
+ assert_body 'no match'
+ get '/', {}, { 'HTTP_ACCEPT' => 'foo/*' }
+ assert_body 'no match'
+ get '/', {}, { 'HTTP_ACCEPT' => 'application/*' }
+ assert_body 'application/xml;charset=utf-8'
+ get '/', {}, { 'HTTP_ACCEPT' => '*/*' }
+ assert_body 'application/xml;charset=utf-8'
+ end
+
+ it 'prefers concrete over partly generic types' do
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/*, text/html' }
+ assert_body 'text/html;charset=utf-8'
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png, text/*' }
+ assert_body 'image/png'
+ end
+
+ it 'prefers concrete over fully generic types' do
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
+ get '/', {}, { 'HTTP_ACCEPT' => '*/*, text/html' }
+ assert_body 'text/html;charset=utf-8'
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png, */*' }
+ assert_body 'image/png'
+ end
+
+ it 'prefers partly generic over fully generic types' do
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
+ get '/', {}, { 'HTTP_ACCEPT' => '*/*, text/*' }
+ assert_body 'text/html;charset=utf-8'
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/*, */*' }
+ assert_body 'image/png'
+ end
+
+ it 'respects quality with generic types' do
+ mock_app { get('/', :provides => [:png, :html]) { content_type }}
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/*;q=1, text/html;q=0' }
+ assert_body 'image/png'
+ get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5, text/*;q=0.7' }
+ assert_body 'text/html;charset=utf-8'
+ end
+
+ it 'accepts both text/javascript and application/javascript for js' do
+ mock_app { get('/', :provides => :js) { content_type }}
+ get '/', {}, { 'HTTP_ACCEPT' => 'application/javascript' }
+ assert_body 'application/javascript;charset=utf-8'
+ get '/', {}, { 'HTTP_ACCEPT' => 'text/javascript' }
+ assert_body 'text/javascript;charset=utf-8'
+ end
+
+ it 'accepts both text/xml and application/xml for xml' do
+ mock_app { get('/', :provides => :xml) { content_type }}
+ get '/', {}, { 'HTTP_ACCEPT' => 'application/xml' }
+ assert_body 'application/xml;charset=utf-8'
+ get '/', {}, { 'HTTP_ACCEPT' => 'text/xml' }
+ assert_body 'text/xml;charset=utf-8'
+ end
+
it 'passes a single url param as block parameters when one param is specified' do
mock_app {
get '/:foo' do |foo|

0 comments on commit 9808369

Please sign in to comment.