Content Handling

The glinda.content module includes functions and classes that make content negotiation in Tornado easier to extend. Tornado’s web framework includes get_body_argument() which handles a handful of body encodings. It is difficult to add new content type handlers in the current framework. Instead of adding all of the logic into the RequestHandler, glinda.content maintains a mapping from content type to encoder and decoder callables and exposes a mix-in that implements content negotiation over a RequestHandler.

The get_request_body() method added by the HandlerMixin class will decode and cache the request body based on a set of registered content types. You register encoder and decoder functions associated with specific content types when your application starts up and the HandlerMixin will call them when it decodes the request body. Request bodies are exposed from Tornado as raw byte strings. Calling register_binary_type() associates binary transcoding functions with a specific MIME content type.

from glinda import content
import msgpack

content.register_binary_type('application/msgpack', msgpack.dumpb,
                             msgpack.loadb)

The transcoding functions are called to translate between dict and byte representations when the Content-Type header matches the specified value.

Many HTTP payloads are text-based and the protocol includes character set negotiation separately from the content type. The character set of the request body is usually indicated by the charset content parameter ala Content-Type: application/json; charset=utf-8. You can register string- based transcoding functions with register_text_type(). Request body processing will decode the byte string into a str instance according to the detected character set before calling text-based decoding functions. If a character set is not included in the request headers, then an application specified default value is used.

from glinda import content
import json

content.register_text_type('application/json', 'utf-8',
                           json.dumps, json.loads)

Binary registrations are preferred over text since they do not require the character transcoding process.

Once you have registered some content handlers, use the HandlerMixin class to de-serialize requests and serialize responses. The following class mimics the GET and POST functionality of the excellent http://httpbin.org utility site.

class HttpbinHandler(content.HandlerMixin, web.RequestHandler):
    """Mimics http://httpbin.org/{get,post}"""

    def get(self):
        self.send_response(self.standard_response_dict)
        self.finish()

    def post(self):
        response = self.standard_response_dict
        response['data'] = repr(self.request.body)
        response['files'] = {}
        response['form'] = {}
        response['body'] = self.get_request_body()
        self.send_response(response)
        self.finish()

    @property
    def standard_response_dict(self):
        return {
            'args': httpcompat.parse_qs(self.request.query),
            'headers': dict(self.request.headers),
            'origin': self.request.remote_ip,
            'url': self.request.uri,
        }

When you run examples/contentneg.py, it will run a Tornado application listening on port 8000 with at least the JSON content handler enabled. If the msgpack module is available, then the application/x-msgpack content type will be enabled. Likewise for the yaml module and application/yaml. Assuming that you have the PyYAML package installed, then the following examples should work.

A request that explicitly requests a JSON response will get one.

GET / HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/0.9.2
HTTP/1.1 200 OK
Content-Length: 204
Content-Type: application/json; charset=utf-8
Date: Sun, 09 Aug 2015 17:00:30 GMT
Etag: "7bccfbf9d3f99b4b9bc88ec4f27b1913e5c0b27e"
Server: TornadoServer/4.2

{
    "args": {},
    "headers": {
        "Accept": "application/json",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Host": "localhost:8000",
        "User-Agent": "HTTPie/0.9.2"
    },
    "origin": "::1",
    "url": "/"
}

If you explicitly request application/yaml, then the same data will be encoded as a YAML document.

GET / HTTP/1.1
Accept: application/yaml
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/0.9.2
HTTP/1.1 200 OK
Content-Length: 174
Content-Type: application/yaml; charset=utf-8
Date: Sun, 09 Aug 2015 17:04:23 GMT
Etag: "3d88b7fc99bb1b31807e88e4ea3d312d391c037b"
Server: TornadoServer/4.2

args: {}
headers: {Accept: application/yaml, Accept-Encoding: 'gzip, deflate',
  Connection: keep-alive, Host: 'localhost:8000', User-Agent: HTTPie/0.9.2}
origin: ::1
url: /

The request handler simple needs to use HandlerMixin.get_request_body() method to retrieve the request body and HandlerMixin.send_response() to transmit a response body.

Functions

glinda.content.register_binary_type(content_type, dumper, loader)[source]

Register handling for a binary content type.

Parameters
  • content_type (str) – content type to register the hooks for

  • dumper – called to decode bytes into a dictionary. Calling convention: dumper(obj_dict) -> bytes.

  • loader – called to encode a dictionary into a byte string. Calling convention: loader(obj_bytes) -> dict

glinda.content.register_text_type(content_type, default_encoding, dumper, loader)[source]

Register handling for a text-based content type.

Parameters
  • content_type (str) – content type to register the hooks for

  • default_encoding (str) – encoding to use if none is present in the request

  • dumper – called to decode a string into a dictionary. Calling convention: dumper(obj_dict).encode(encoding) -> bytes

  • loader – called to encode a dictionary to a string. Calling convention: loader(obj_bytes.decode(encoding)) -> dict

The decoding of a text content body takes into account decoding the binary request body into a string before calling the underlying dump/load routines.

glinda.content.clear_handlers()[source]

Clears registered type handlers.

Classes

class glinda.content.HandlerMixin(*args, **kwargs)[source]

Mix this in over RequestHandler to enable content handling.

get_request_body()[source]

Decodes the request body and returns it.

Returns

the decoded request body as a dict instance.

Raises

tornado.web.HTTPError if the body cannot be decoded (415) or if decoding fails (400)

registered_content_types

Yields the currently registered content types in some order.

send_response(response_dict)[source]

Encode a response according to the request.

Parameters

response_dict (dict) – the response to send

Raises

tornado.web.HTTPError if no acceptable content type exists

This method will encode response_dict using the most appropriate encoder based on the Accept request header and the available encoders. The result is written to the client by calling self.write after setting the response content type using self.set_header.