PHP: HTTP content negotiation

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:

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 for further reading:

Written by Christian Weiske.

Comments? Please send an e-mail.