ニシキヘビってかわいいよね、実際みたことないけど。

無職がいよかん国でプログラミングとかの備忘録を書いてます。 一日一食たまごかけごはん。

TornadoでJSONを吐くAPI作るときにつかうことあれこれ

久しぶりにコード書くと、細かいところが思い出せない......

  • ヘッダの値を読み取る

RequestHandlerのrequest.headersプロパティに、ヘッダ名がキーの辞書形式で保存されている。 例はX-HTTP-Method-Overrideの値を取得するところ。 もちろんヘッダが存在しないとValueError例外が出るので、Dictのget()メソッドなりで対処する。

method_override = self.request.headers['X-HTTP-Method-Override']
  • 全HTTPメソッドの共通処理をつけくわえる。

RequestHandler.perpare() をオーバーライドして、処理を書く。
例えば、application/json以外のコンテンツを受け取る気がない場合は

class JSONAPIHandler(RequestHandler):

    def prepare(self):
        if self.request.headers.get('Content-Type') != 'application/json':
            raise HTTPError(406)
  • JSONを受け取る

tornado.escape.json_decode()でbodyを辞書に変換すればOK。

import tornado.escape
# 中略

class JSONAPIHandler(RequestHandler):

    def post(self, *arg, **kwargs):
        request_json = json_decode(self.request.body)

tornado.escape.json_encode()でJSONにした文字列をRequestHandler.finish()やRequestHandler.write()でもいいが、 これら2つのメソッドに渡すオブジェクトが辞書の場合、内部でJSONエンコードしてくれるので便利。
ついでにContent-Typeヘッダの値をapplication/jsonに変更してれるため使えるなら使おう。

RequestHandler.write()のソースを引用

    def write(self, chunk):
        """
        pydocは略
        """
        if self._finished:
            raise RuntimeError("Cannot write() after finish()")
        if not isinstance(chunk, (bytes, unicode_type, dict)):
            message = "write() only accepts bytes, unicode, and dict objects"
            if isinstance(chunk, list):
                message += ". Lists not accepted for security reasons; see http://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.write"
            raise TypeError(message)
        if isinstance(chunk, dict):
            chunk = escape.json_encode(chunk)
            self.set_header("Content-Type", "application/json; charset=UTF-8")
        chunk = utf8(chunk)
        self._write_buffer.append(chunk)

見てのとおり、JSONのオブジェクトでラップせずにリストを返信しようとすると、セキュリティ上の観点からダメと怒られるので注意。

  • エラーもJSONにする

HTTPErrorや内部例外で5XXエラーを返すとき、デフォルトのままだとテキストがレスポンスされる。 JSONしか返す気のないAPIをつくる際には、ちょっと気持ち悪い RequestHandlerのwrite_error()メソッドをオーバーライドする。
自分は元のコードをベースにしてこんなのを使っている。

# 前略

    def write_error(self, status_code, **kwargs):

        res = OrderedDict([
            ('code', status_code),
            ('message', self._reason),
        ])

        if self.settings.get("serve_traceback") and "exc_info" in kwargs:
            res['exc_info'] = '\n'.join(traceback.format_exception(*kwargs["exc_info"]))

        self.finish(res)