Fork me on GitHub

Content Negotiation

HTTP supports a rich set of methods to negotiate the representation of a resource. The client tells the server its preferences on media-type, character set, content encoding and language and the server uses this information to select the best representation available.

Negotiating a media type

A typical use case with liberator is the support of multiple media types. Out of the box, liberator supports response generation for arbitrary clojure values such as JSON, clojure forms and some others. You can also return a String, File or InputStream from a handler which will be returned basically literally.

If you want to negotiate on the media type, then define the key :available-media-types which must return a list of the supported media-type (or be a function which returns the list at runtime). Liberator will then determine, based on this list and the request header Accept, which is the best media type available.

Liberator will store the media type in the representation map in the context. This map is available in the context at the key :representation. You can use the values in any handler function that is called after the media type was evaluated. This is done in the decision called :media-type-available?.

You can also specify a function for the key :media-type-available? instead of :available-media-types. The default implementation uses :available-media-types to gain a list of possible types does the negotiation with the Accept header and stores the outcome in the representation map. This is in most cases more convenient than doing this manually.

An example will illustrate how things fit together:

  (ANY "/babel" []
       (resource :available-media-types ["text/plain" "text/html"
                                         "application/json" "application/clojure;q=0.9"]
                 :handle-ok
                 #(let [media-type
                        (get-in % [:representation :media-type])]
                    (condp = media-type
                      "text/plain" "You requested plain text"
                      "text/html" "<html><h1>You requested HTML</h1></html>"
                      {:message "You requested a media type"
                       :media-type media-type}))
                 :handle-not-acceptable "Uh, Oh, I cannot speak those languages!"))

Playtime

Let’s try some request

$ curl -v http://localhost:3000/babel
* About to connect() to localhost port 3000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 3000 (#0)
> GET /babel HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:3000
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 23 Apr 2013 06:49:54 GMT
< Vary: Accept
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 24
< Server: Jetty(7.6.1.v20120215)
<
* Connection #0 to host localhost left intact
You requested plain text* Closing connection #0

You can see that curl sent an Accept header of “*/*” which means that it accepts any media type. In this case, liberator will return the first available media type.

Let’s try to be more specific and tell that we accept json and clojure but prefer clojure. (This was expected, right?)

curl -v -H "Accept: application/json=0.8,application/clojure" http://localhost:3000/babel
* About to connect() to localhost port 3000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 3000 (#0)
> GET /babel HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:3000
> Accept: application/json=0.8,application/clojure
>
< HTTP/1.1 200 OK
< Date: Tue, 23 Apr 2013 06:52:32 GMT
< Vary: Accept
< Content-Type: application/clojure;charset=UTF-8
< Content-Length: 117
< Server: Jetty(7.6.1.v20120215)
<
* Connection #0 to host localhost left intact
#=(clojure.lang.PersistentArrayMap/create {:message "You requested a media type", :media-type "application/clojure"})* Closing connection #0

You can see that we received a clojure representation. The representation was automatically generated by liberator from the clojure map that was returned from the handler method.

So, what happens if we request some media-type which is not available? As you can guess, liberator will finally use :handle-not-acceptable to generate a 406 response:

curl -v -H 'Accept: image/png' http://localhost:3000/babel
* About to connect() to localhost port 3000 (#0)
*   Trying ::1...
* connected
* Connected to localhost (::1) port 3000 (#0)
> GET /babel HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:3000
> Accept: image/png
>
< HTTP/1.1 406 Not Acceptable
< Date: Tue, 23 Apr 2013 06:58:15 GMT
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 39
< Server: Jetty(7.6.1.v20120215)
<
* Connection #0 to host localhost left intact
Uh, Oh, I cannot speak those languages!* Closing connection #0

Vary

If you watched closely then you observed that liberator automatically returns a value for the Vary header. The value of the header is filled from the negotiated parameters in the representation map. If you negotiated on e.g. media-type and language then it will be set to Accept, Accept-Language. The header is used by caching proxies and caching user agents to tell apart different representation of the same resource. Thus it is vital that it is set correctly. If not, clients will receive cached values in the wrong media-type and worse.

More negotiable parameters

Liberator supports negotiation for the headers Accept (media-type), Accept-Language, Accept-Charset and Accept-Encoding. The negotiated values are stored in the representation map at the keys :media-type, :language, :charset and :encoding.

If a handler returns a string for the representation then liberator will return an inputstream for a bytestream representation of the string in the negotiated character set. You can use (keys (java.nio.charset.Charset/availableCharsets)) to obtain the supported character sets on your platform.

Automatic character encoding is only supported if a handler returns a string value. If it returns an inputstream then liberator expect it to be already encoded correctly. Re-encoding is, while technically possible, in general a costly and unnecessary operation. In the case that you absolutely need it you can re-encode an inputstream in the handler.

The parameter :encoding has no further support from liberator. While content-encoding will typically be done by a reverse proxy in front of your application, nothing prevents you to return a compressed representation in the handler if requested.

Continue with Conditional Requests.