With HTTP, resources are identified using URIs. And a uniquely identified resource might support multiple resource representations. A representation is a specific form of a particular resource.
For example:
- a HTML page /index.html might be available in different languages
- product data located at /products/123 can be served in JSON, XML or CSV
- an avatar image /user/avatar might available in JPEG, PNG and GIF formats
In all these cases one underlying resource has multiple different representations.
Content negotiation is the mechanism used by clients and servers to decide which representation should be used.
Server-driven and agent-driven content negotiation
We can differentiate between server-driven and agent-driven content negotiation.
With server-driven content negotiation the client tells the server which representations are preferable. The server then picks the representation that best fits the clients needs.
When using agent-driven content negotiation the server tells the client which representations are available. The client then picks the best matching option.
In practice nearly only server-driven negotiation is used. Unfortunately, there is no standardized format for doing agent-driven negotiation. Additionally, agent-driven negotiation is usually also worse for performance as it requires an additional request / response round trip. In the rest of this article we will therefore focus on server-driven negotiation.
Accept headers
With server-driven negotiation the client uses headers to indicate supported content formats. A server-side algorithm then uses these headers to decide which resource representation should be returned.
Most commonly used is the Accept-Header, which communicates the media-type preferred by the client. For example, consider the following simple HTTP request containing an Accept header:
GET /monthly-report Accept: text/html; q=1.0, text/*; q=0.8
The header tells the server that the client understands HTML (media-type text/html) and other text based formats (mediatype text/*).
text/* indicates that all subtypes of the text type are supported. To indicate that all media types are supported we can use */*.
In this example HTML is preferred over other text based formats because it has a higher quality factor (q).
Ideally a server would respond with a HTML document to this request. For example:
HTTP/1.1 200 OK Content-Type: text/html <html> <body> <h1>Monthly report</h1> ... </body> </html>
If returning HTML is not feasible, the server can also respond with another text based format, like text/plain:
200 OK Content-Type: text/plain Monthly report Bla bli blu ...
Besides the Accept header there are also the Accept-Language and Accept-Encoding headers, we can use. Accept-Language indicates the language preference of the client while Accept-Encoding defines the acceptable content encodings.
Of course all these headers can be used together. For example:
GET /monthly-report Accept: text/html Accept-Language: en-US; q=1.0, en; q=0.9, fr; q=0.4 Accept-Encoding: gzip, br
Here the client indicates that he prefers
- an HTML document
- US English (preferred, q=1.0) but other English variations are also fine (q=0.9). If English is not available, French can do the job too (q=0.4)
- gzip and brotli (br) encoding is supported
An acceptable response might look like this:
200 Ok Content-Type: text/html Content-Language: en Content-Encoding: gzip <gzipped html document>
What if the server cannot return an acceptable response?
If the server is unable to fulfill the clients preferences the HTTP status code 406 (Not Acceptable) can be returned. This status code indicates that the server is unable to produce a response matching the clients preference.
Depending on the situation it might also be viable to return a response that does not exactly match the clients preference. For example, assume no language provided in the Accept-Language header is supported by the server. In this case, it can still be a valid option to return a response using a predefined default language. This might be more useful for the client than nothing. In this case, the client can look at the Content-Language header of the response and decide if he wants to use the response or ignore it.
Content negotiation in REST APIs
For REST APIs it can be a viable option to support more than one standard representation for resources. For example, with content negotiation we can support JSON and XML and let the client decide what he wants to use.
CSV can also be an interesting option to consider in certain situations as the response can directly be viewed with tools like Excel. For example, consider the following request:
GET /users Accept: text/csv
Instead of returning a JSON (or XML) collection, the server now can respond with a list of users in CSV format.
HTTP/1.1 200 Ok Content-Type: text/csv Id;Username;Email 1;john;john.doe@example.com 2;anna91;anna91@example.com
Leave a reply