HTTP requests contain header that explain which data the client accepts and is able to understand: Type of the content (Accept), language (Accept-Language), charset and compression (encoding).
By leveraging this header values, your web application can automatically deliver content in the correct language. Using content types in the Accept headers, your REST API doesn't need versioned URLs but can react differently on the same URL.
Header value structure
Acceptance headers are comma-separated list of values with optional extension data. One additional data point - quality - determines a ranking order between the values.
Simple header
Accept: image/png, image/jpeg, image/gif
Here the HTTP client expresses that he understands only content of MIME types image/png, image/jpeg and image/gif.
Quality
Accept: image/png, image/jpeg;q=0.8, image/gif;q=0.5
Both image/jpeg and image/gif have a quality value now. jpeg's 0.8 is higher than gif's 0.5, so jpeg is preferred over gif. image/png has no explicit quality value, so the default quality of 1 is used. This means that in the end, png is preferred over jpeg, which is preferred over gif.
So if the server has the data available in two formats .png and .jpeg, it should send the png file to the client.
Quality values may appear in any order:
Accept: image/gif;q=0.5, image/png, image/jpeg;q=0.8
Extensions
Apart from the q quality extension, other tokens may be used:
Accept: text/html;video=0, text/html;q=0.9
In this example, the client prefers to get the HTML page without videos, but also falls back to the "normal" HTML page. (Note that this is an fictive example. There is no video token standardized anywhere.)
Parsing header values
Parsing and interpreting the Accept* headers is not simply an explode() call, but you also need to strip away the extensions and order the values by their quality.
Instead of implementing this all yourself, you can rely on a the stable and unit-tested library HTTP2 from PEAR.
Installation is simple:
$ pear install HTTP2-beta
To use it, simply require HTTP2.php:
require_once 'HTTP2.php';
pecl_http
The PHP Extension Community Library has an extension pecl_http which provides functions for HTTP content negotiation.
If you're on a shared host, you'll have to ask your hoster/admin to install this extension. In this case, you're better of with the HTTP2 PEAR package since that can be installed without admin access .
Other libraries
There are other libraries that implement HTTP content negotiation in PHP:
Content type
The type of content is determined via the Accept header. It contains a list of MIME types that the HTTP client understands.
Apart from full MIME types, partial ones are allowed:
- image/* would allow all images
- */* allows everything
If you cannot provide the content type the client requests, you should return a 406 Not Acceptable HTTP status code.
Parsing the Accept header
The HTTP2 package has full support for partial types and quality values.
<?php require_once 'HTTP2.php'; $http = new HTTP2(); $supportedTypes = array( 'application/xhtml+xml', 'text/html', 'application/atom+xml', 'application/json' ); $type = $http->negotiateMimeType($supportedTypes, false); if ($type === false) { header('HTTP/1.1 406 Not Acceptable'); echo "You don't want any of the content types I have to offer\n"; } else { echo 'I\'d give you data of type: ' . $type . "\n"; } ?>
You can test the script with a command line client like curl:
$ curl -iH 'Accept: foo' http://localhost/content-type.php HTTP/1.1 406 Not Acceptable ... You don't want any of the content types I have to offer
$ curl -iH 'Accept: text/html' http://localhost/content-type.php HTTP/1.1 200 OK ... I'd give you data of type: text/html
$ curl -iH 'Accept: application/*' http://localhost/content-type.php HTTP/1.1 200 OK ... I'd give you data of type: application/xhtml+xml
Try it at demo/php-http-negotiation/content-type.php.
Language
HTTP clients may express the language the user understands with the Accept-Language header.
It consists of a case-insensitive list of language tags, which may be either two-letter ISO-639 language codes (like en, de, fr) or a combination of two-letter language codes and two-letter ISO-3166 country codes, separated by a dash: en-us, de-DE, de-AT.
Remember that they may include extensions and quality values.
Examples
Accept-Language: en-US,en;q=0.8
Accept-Language: de-DE, de;q=0.9, en-US;q=0.6, en;q=0.5
Parsing language codes
When parsing the requested language, it is sensible to fall back to a default language. Not giving out content, because the accepted languages do not match, makes not much sense in most cases.
The following code uses a fallback language en if none of the allowed ones matches the user's preferences.
<?php require_once 'HTTP2.php'; $http = new HTTP2(); $supportedLanguages = array( 'de' => 'de', 'de-DE' => 'de', 'de-AT' => 'de', 'en' => 'en', 'en-US' => 'en', 'en-UK' => 'en', ); $language = $http->negotiateLanguage($supportedLanguages, 'en'); echo 'I\'d give you text of language: ' . $language . "\n"; ?>
Try it at demo/php-http-negotiation/language.php.
Charset
Apart from content type and language, the client may limit the accepted response charsets (what most people would call "encoding"): utf-8, iso-8859-1 - with Accept-Charset
It's not used that often anymore, and not sent to the server by Opera 12.15, Chromium 28 and Firefox 22.
You may use HTTP2's negotiateCharset() method to determine the preferred charset.
Links
Links for further reading:
- Content Negotiation: why it is useful, and how to make it work - describes a solution to use a user-configurable language setting together with language negotiation if the user did not select a language beforehand.