Content Negotiation

What is Content Negotiation?

Content negotiation is a way to determine what type of content to return to the client based on what the client can handle, and what the server can handle. This can be used to determine whether the client is wanting HTML or JSON returned, whether the image should be returned as a JPEG or PNG, what type of compression is supported and more. This is done by analyzing four different headers which can each support multiple value options, each with their own priority.

Trying to match this up manually can be pretty challenging. CodeIgniter provides the Negotiator class that can handle this for you.

At it’s heart Content Negotiation is simply a part of the HTTP specification that allows a single resource to serve more than one type of content, allowing the clients to request the type of data that works best for them.

A classic example of this is a browser that cannot display PNG files can request only GIF or JPEG images. When the server receives the request, it looks at the available file types the client is requesting and selects the best match from the image formats that it supports, in this case likely choosing a JPEG image to return.

This same negotiation can happen with four types of data:

  • Media/Document Type - this could be image format, or HTML vs. XML or JSON.

  • Character Set - The character set the returned document should be set in. Typically is UTF-8.

  • Document Encoding - Typically the type of compression used on the results.

  • Document Language - For sites that support multiple languages, this helps determine which to return.

Loading the Class

You can load an instance of the class manually through the Service class:

<?php

$negotiate = service('negotiator');

This will grab the current request instance and automatically inject it into the Negotiator class.

This class does not need to be loaded on it’s own. Instead, it can be accessed through this request’s IncomingRequest instance. While you cannot access it directly this way, you can easily access all of methods through the negotiate() method:

<?php

$request->negotiate('media', ['foo', 'bar']);

When accessed this way, the first parameter is the type of content you’re trying to find a match for, while the second is an array of supported values.

Negotiating

In this section, we will discuss the 4 types of content that can be negotiated and show how that would look using both of the methods described above to access the negotiator.

Media

The first aspect to look at is handling ‘media’ negotiations. These are provided by the Accept header and is one of the most complex headers available. A common example is the client telling the server what format it wants the data in. This is especially common in APIs. For example, a client might request JSON formatted data from an API endpoint:

GET /foo HTTP/1.1
Accept: application/json

The server now needs to provide a list of what type of content it can provide. In this example, the API might be able to return data as raw HTML, JSON, or XML. This list should be provided in order of preference:

<?php

$supported = [
    'application/json',
    'text/html',
    'application/xml',
];

$format = $request->negotiate('media', $supported);
// or
$format = $negotiate->media($supported);

In this case, both the client and the server can agree on formatting the data as JSON so ‘json’ is returned from the negotiate method. By default, if no match is found, the first element in the $supported array would be returned. In some cases, though, you might need to enforce the format to be a strict match. If you pass true as the final value, it will return an empty string if no match is found:

<?php

$format = $request->negotiate('media', $supported, true);
// or
$format = $negotiate->media($supported, true);

Language

Another common usage is to determine the language the content should be served in. If you are running only a single language site, this obviously isn’t going to make much difference, but any site that can offer up multiple translations of content will find this useful, since the browser will typically send the preferred language along in the Accept-Language header:

GET /foo HTTP/1.1
Accept-Language: fr; q=1.0, en; q=0.5

In this example, the browser would prefer French, with a second choice of English. If your website supports English and German you would do something like:

<?php

$supported = [
    'en',
    'de',
];

$lang = $request->negotiate('language', $supported);
// or
$lang = $negotiate->language($supported);

In this example, ‘en’ would be returned as the current language. If no match is found, it will return the first element in the $supported array, so that should always be the preferred language.

Strict Locale Negotiation

New in version 4.6.0.

By default, locale is determined on a lossy comparison basis. So only the first part of the locale string is taken into account (language). This is usually sufficient. But sometimes we want to be able to distinguish between regional versions such as en-US and en-GB to serve different content.

For such cases, we have introduced a new setting that can be enabled via Config\Feature::$strictLocaleNegotiation. This will ensure that the strict comparison will be made in the first place.

Note

CodeIgniter comes with translations only for primary language tags (‘en’, ‘fr’, etc.). So if you enable this feature and your settings in Config\App::$supportedLocales include regional language tags (‘en-US’, ‘fr-FR’, etc.), then keep in mind that if you have your own translation files, you must also change the folder names for CodeIgniter’s translation files to match what you put in the $supportedLocales array.

Now let’s consider the below example. The browser’s preferred language will be set as this:

GET /foo HTTP/1.1
Accept-Language: fr; q=1.0, en-GB; q=0.5

In this example, the browser would prefer French, with a second choice of English (United Kingdom). Your website on another hand supports German and English (United States):

<?php

$supported = [
    'de',
    'en-US',
];

$lang = $request->negotiate('language', $supported);
// or
$lang = $negotiate->language($supported);

In this example, ‘en-US’ would be returned as the current language. If no match is found, it will return the first element in the $supported array. Here is how exactly the locale selection process works.

Even though the ‘fr’ is preferred by the browser it is not in our $supported array. The same problem occurs with ‘en-GB’, but here we will be able to search for variants. First, we will fallback to the most general locale (in this case ‘en’) which again is not in our array. Then we will search for the regional locale ‘en-’. And that’s when our value from the $supported array will be matched. We will return ‘en-US’.

So the process of selecting a locale is as follows:

  1. strict match (‘en-GB’) - ISO 639-1 plus ISO 3166-1 alpha-2

  2. general locale match (‘en’) - ISO 639-1

  3. regional locale match (‘en-’) - ISO 639-1 plus “wildcard” for ISO 3166-1 alpha-2

Encoding

The Accept-Encoding header contains the character sets the client prefers to receive, and is used to specify the type of compression the client supports:

GET /foo HTTP/1.1
Accept-Encoding: compress, gzip

Your web server will define what types of compression you can use. Some, like Apache, only support gzip:

<?php

$type = $request->negotiate('encoding', ['gzip']);
// or
$type = $negotiate->encoding(['gzip']);

See more at Wikipedia.

Character Set

The desired character set is passed through the Accept-Charset header:

GET /foo HTTP/1.1
Accept-Charset: utf-16, utf-8

By default, if no matches are found, utf-8 will be returned:

<?php

$charset = $request->negotiate('charset', ['utf-8']);
// or
$charset = $negotiate->charset(['utf-8']);