AcceptHeaders is a ruby library that does content negotiation and parses and sorts http accept headers.
Some features of the library are:
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
[
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
.
media_types.negotiate(['text/html', 'text/plain'])
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']
#accept?
:
media_types.accept?('text/html')
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
[
AcceptHeaders::Encoding.new('gzip'),
AcceptHeaders::Encoding.new('compress', q: 0.8),
AcceptHeaders::Encoding.new('deflate', q: 0.5)
]
#negotiate
:
encodings.negotiate(['gzip', 'compress'])
AcceptHeader::Encoding.new('gzip')
#accept?
:
encodings.accept?('gzip')
encodings.accept?('identity')
Accept-Language
Accept::Language::Negotiator
:
accept_language = 'Accept-Language: en-*, en-us, *;q=0.8'
languages = AcceptHeaders::Language::Negotiator.new(accept_language)
languages.list
[
AcceptHeaders::Language.new('en', 'us'),
AcceptHeaders::Language.new('en', '*'),
AcceptHeaders::Language.new('*', '*', q: 0.8)
]
#negotiate
:
languages.negotiate(['en-us', 'zh-Hant'])
AcceptHeaders::Language.new('en', 'us')
#accept?
:
languages.accept?('en-gb')
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)
supported_media_types = %w[application/json application/xml text/html text/plain]
supported_encodings = %w[gzip identify]
supported_languages = %w[en-US en-GB]
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)
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