AcceptHeaders
AcceptHeaders is a ruby library that does content negotiation and parses and sorts http accept headers.
Some features of the library are:
- Strict adherence to RFC 2616, specifically section 14
- Full support for the Accept, Accept-Encoding, and Accept-Language HTTP request headers
-
Accept-Charset
is not supported because it's obsolete - Parser tested against all IANA registered media types and encodings
- A comprehensive spec suite that covers many edge cases
This library is optimistic when parsing headers. If a specific media type, encoding, or language can't be parsed, is in an invalid format, or contains invalid characters, it will skip that specific entry when constructing the sorted list. If a q
value can't be read or is in the wrong format (more than 3 decimal places), it will default it to 0.001
so it still has a chance to match. Lack of an explicit q
value of course defaults to 1.
Installation
Add this line to your application's Gemfile:
gem 'accept_headers'
And then execute:
$ bundle
Or install it yourself as:
$ gem install accept_headers
Usage
Accept
AcceptHeaders::MediaType::Negotiator
is a class that is initialized with an Accept
header string and will internally store an array of MediaType
s in descending order according to the spec, which takes into account q
value, type
/subtype
and extensions
specificity.
accept_header = 'Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5'
media_types = AcceptHeaders::MediaType::Negotiator.new(accept_header)
media_types.list
# Returns:
[
AcceptHeaders::MediaType.new('text', 'html', extensions: { 'level' => '1' }),
AcceptHeaders::MediaType.new('text', 'html', q: 0.7),
AcceptHeaders::MediaType.new('*', '*', q: 0.5),
AcceptHeaders::MediaType.new('text', 'html', q: 0.4, extensions: { 'level' => '2' }),
AcceptHeaders::MediaType.new('text', '*', q: 0.3)
]
#negotiate
takes an array of media range strings supported (by your API or route/controller) and returns the best supported MediaType
and the extensions
params from the matching internal media type.
This will first check the available list for any matching media types with a q
of 0 and skip any matches. It does this because the RFC specifies that if the q
value is 0, then content with this parameter is not acceptable
. Then it'll look to the highest q
values and look for matches in descending q
value order and return the first match (accounting for wildcards). Finally, if there are no matches, it returns nil
.
# The same media_types variable as above
media_types.negotiate(['text/html', 'text/plain'])
# Returns this equivalent:
AcceptHeader::MediaType.new('text', 'html', extensions: { 'level' => '1' })
It returns the matching MediaType
, so you can see which one matched and also access the extensions
params. For example, if you wanted to put your API version in the extensions, you could then retrieve the value.
versions_header = 'Accept: application/json;version=2,application/json;version=1;q=0.8'
media_types = AcceptHeaders::MediaType::Negotiator.new(versions_header)
m = media_types.negotiate('application/json')
puts m.extensions['version'] # returns '2'
#accept?
:
media_types.accept?('text/html') # true
Accept-Encoding
AcceptHeader::Encoding::Encoding
:
accept_encoding = 'Accept-Encoding: deflate; q=0.5, gzip, compress; q=0.8, identity'
encodings = AcceptHeaders::Encoding::Negotiator.new(accept_encoding)
encodings.list
# Returns:
[
AcceptHeaders::Encoding.new('gzip'),
AcceptHeaders::Encoding.new('compress', q: 0.8),
AcceptHeaders::Encoding.new('deflate', q: 0.5)
]
#negotiate
:
encodings.negotiate(['gzip', 'compress'])
# Returns this equivalent:
AcceptHeader::Encoding.new('gzip')
#accept?
:
encodings.accept?('gzip') # true
# Identity is accepted as long as it's not explicitly rejected 'identity;q=0'
encodings.accept?('identity') # true
Accept-Language
Accept::Language::Negotiator
:
accept_language = 'Accept-Language: en-*, en-us, *;q=0.8'
languages = AcceptHeaders::Language::Negotiator.new(accept_language)
languages.list
# Returns:
[
AcceptHeaders::Language.new('en', 'us'),
AcceptHeaders::Language.new('en', '*'),
AcceptHeaders::Language.new('*', '*', q: 0.8)
]
#negotiate
:
languages.negotiate(['en-us', 'zh-Hant'])
# Returns this equivalent:
AcceptHeaders::Language.new('en', 'us')
#accept?
:
languages.accept?('en-gb') # true
Rack Middleware
Add the middleware:
require 'accept_headers/middleware'
use AcceptHeaders::Middleware
run YourApp
Simple way to set the content response headers based on the request accept headers and the supported media types, encodings, and languages provided by the app or route.
class YourApp
def initialize(app)
@app = app
end
def call(env)
# List your arrays of supported media types, encodings, languages. This can be global or per route/controller
supported_media_types = %w[application/json application/xml text/html text/plain]
supported_encodings = %w[gzip identify]
supported_languages = %w[en-US en-GB]
# Call the Negotiators and pass in the supported arrays and it'll return the best match
matched_media_type = env["accept_headers.media_types"].negotiate(supported_media_types)
matched_encoding = env["accept_headers.encodings"].negotiate(supported_encodings)
matched_language = env["accept_headers.languages"].negotiate(supported_languages)
# Set a default, in this case an empty string, in case of a bad header that cannot be parsed
# The return value is a MediaType, Encoding, or Language depending on the case:
# On MediaType, you can call #type ('text'), #subtype ('html'), #media_range ('text/html') to get the stringified parts
# On Encoding, you can call #encoding to get the string encoding ('gzip')
# On Language, you can call #primary_tag ('en'), #subtag ('us'), or #language_tag ('en-us')
headers = {
'Content-Type' => matched_media_type ? matched_media_type.media_range : '',
'Content-Encoding' => matched_encoding ? matched_encoding.encoding : '',
'Content-Language' => matched_language ? matched_language.language_tag : '',
}
[200, headers, ["Hello World!"]]
end
end
Contributing
- Fork it ( https://github.com/[my-github-username]/accept_headers/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request