Tornado ウェブアプリケーションの構造

Tornado ウェブアプリケーションは通常、1つ以上の RequestHandler サブクラス、受信リクエストをハンドラーにルーティングする Application オブジェクト、およびサーバーを起動する main() 関数で構成されます。

最小限の「hello world」の例は次のようになります。

import asyncio
import tornado

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

async def main():
    app = make_app()
    app.listen(8888)
    shutdown_event = asyncio.Event()
    await shutdown_event.wait()

if __name__ == "__main__":
    asyncio.run(main())

main コルーチン

Tornado 6.2 および Python 3.10 以降では、Tornado アプリケーションを起動するための推奨パターンは、asyncio.run で実行される main コルーチンを作成することです。(以前のバージョンでは、通常の関数で初期化を行い、IOLoop.current().start() でイベントループを開始するのが一般的でした。ただし、このパターンは Python 3.10 以降で非推奨の警告を生成し、将来の Python のバージョンでは動作しなくなります。)

main 関数が戻るとプログラムは終了するため、ウェブサーバーの場合、ほとんどの場合 main は永続的に実行する必要があります。 asyncio.Event を待機し、その set() メソッドが呼び出されないようにすることは、非同期関数を永続的に実行するための便利な方法です。(また、グレースフルシャットダウン手順の一部として main を早期に終了させたい場合は、shutdown_event.set() を呼び出して終了させることができます)。

Application オブジェクト

Application オブジェクトは、リクエストをハンドラーにマッピングするルーティングテーブルなど、グローバルな設定を担当します。

ルーティングテーブルは URLSpec オブジェクト (またはタプル) のリストであり、各オブジェクトには (少なくとも) 正規表現とハンドラークラスが含まれています。順序が重要で、最初に一致するルールが使用されます。正規表現にキャプチャグループが含まれている場合、これらのグループは _パス引数_ であり、ハンドラーの HTTP メソッドに渡されます。辞書が URLSpec の3番目の要素として渡される場合、それは RequestHandler.initialize に渡される _初期化引数_ を提供します。最後に、URLSpec には名前を付けることができ、これにより RequestHandler.reverse_url で使用できるようになります。

たとえば、このフラグメントでは、ルート URL /MainHandler にマッピングされ、/story/ の形式の URL の後に続く数値が StoryHandler にマッピングされます。その数値は (文字列として) StoryHandler.get に渡されます。

class MainHandler(RequestHandler):
    def get(self):
        self.write('<a href="%s">link to story 1</a>' %
                   self.reverse_url("story", "1"))

class StoryHandler(RequestHandler):
    def initialize(self, db):
        self.db = db

    def get(self, story_id):
        self.write("this is story %s" % story_id)

app = Application([
    url(r"/", MainHandler),
    url(r"/story/([0-9]+)", StoryHandler, dict(db=db), name="story")
    ])

Application コンストラクターは、アプリケーションの動作をカスタマイズしたり、オプション機能を有効にしたりするために使用できる多くのキーワード引数を受け取ります。完全なリストについては、Application.settings を参照してください。

RequestHandler のサブクラス化

Tornado ウェブアプリケーションの作業のほとんどは、RequestHandler のサブクラスで行われます。ハンドラーサブクラスの主なエントリポイントは、処理される HTTP メソッドにちなんで名付けられたメソッド (get()post() など) です。各ハンドラーは、異なる HTTP アクションを処理するために、これらのメソッドの1つ以上を定義できます。上記のように、これらのメソッドは、一致したルーティングルールのキャプチャグループに対応する引数を付けて呼び出されます。

ハンドラー内では、RequestHandler.renderRequestHandler.write などのメソッドを呼び出して、応答を生成します。render() は、名前で Template をロードし、指定された引数を使用してレンダリングします。write() は、テンプレートベースではない出力に使用されます。文字列、バイト、および辞書を受け入れます (辞書は JSON としてエンコードされます)。

RequestHandler の多くのメソッドは、サブクラスでオーバーライドしてアプリケーション全体で使用するように設計されています。write_errorget_current_user などのメソッドをオーバーライドする BaseHandler クラスを定義し、特定のすべてのハンドラーに対して RequestHandler の代わりに独自の BaseHandler をサブクラス化するのが一般的です。

リクエスト入力の処理

リクエストハンドラーは、self.request を使用して、現在のリクエストを表すオブジェクトにアクセスできます。属性の完全なリストについては、HTTPServerRequest のクラス定義を参照してください。

HTML フォームで使用される形式のリクエストデータは解析され、get_query_argumentget_body_argument などのメソッドで利用できるようになります。

class MyFormHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('<html><body><form action="/myform" method="POST">'
                   '<input type="text" name="message">'
                   '<input type="submit" value="Submit">'
                   '</form></body></html>')

    def post(self):
        self.set_header("Content-Type", "text/plain")
        self.write("You wrote " + self.get_body_argument("message"))

HTML フォームエンコーディングでは、引数が単一の値なのか、1つの要素を持つリストなのかが曖昧であるため、RequestHandler には、アプリケーションがリストを期待するかどうかを示すための個別のメソッドがあります。リストの場合は、単数形に対応するものの代わりに、get_query_arguments および get_body_arguments を使用します。

フォームを介してアップロードされたファイルは self.request.files で利用できます。これは、名前 (HTML の <input type="file"> 要素の名前) をファイルのリストにマッピングします。各ファイルは、{"filename":..., "content_type":..., "body":...} の形式の辞書です。files オブジェクトは、ファイルがフォームラッパー (つまり、multipart/form-data Content-Type) でアップロードされた場合にのみ存在します。この形式が使用されていない場合、生のアップロードされたデータは self.request.body で利用できます。デフォルトでは、アップロードされたファイルは完全にメモリにバッファリングされます。メモリに快適に保持するには大きすぎるファイルを処理する必要がある場合は、stream_request_body クラスデコレータを参照してください。

demos ディレクトリでは、file_receiver.py に、ファイルのアップロードを受信する両方のメソッドが示されています。

HTMLフォームのエンコーディングの癖(例えば、単数または複数の引数に関する曖昧さなど)により、Tornadoはフォーム引数を他の種類の入力と統合しようとはしません。特に、JSONリクエストボディを解析することはありません。フォームエンコードの代わりにJSONを使用したいアプリケーションは、prepareをオーバーライドしてリクエストを解析することができます。

def prepare(self):
    if self.request.headers.get("Content-Type", "").startswith("application/json"):
        self.json_args = json.loads(self.request.body)
    else:
        self.json_args = None

RequestHandlerメソッドのオーバーライド

get()/post()などの他に、RequestHandlerの特定のメソッドは、必要に応じてサブクラスによってオーバーライドされるように設計されています。すべてのリクエストにおいて、以下の呼び出しシーケンスが行われます。

  1. 新しいRequestHandlerオブジェクトがリクエストごとに作成されます。

  2. initialize()が、Application設定からの初期化引数とともに呼び出されます。initializeは通常、渡された引数をメンバ変数に保存するだけです。出力を行ったり、send_errorのようなメソッドを呼び出したりすることはできません。

  3. prepare()が呼び出されます。これは、すべてのハンドラーサブクラスで共有されるベースクラスで最も役立ちます。prepareは、どのHTTPメソッドが使用されても呼び出されるためです。prepareは出力を行うことができます。もしfinish(またはredirectなど)を呼び出すと、ここで処理が停止します。

  4. HTTPメソッドのうちの1つ(get()post()put()など)が呼び出されます。URL正規表現がキャプチャグループを含んでいる場合、それらはこのメソッドへの引数として渡されます。

  5. リクエストが完了すると、on_finish()が呼び出されます。これは通常、get()または別のHTTPメソッドが戻った後です。

オーバーライドされるように設計されたすべてのメソッドは、RequestHandlerドキュメントでそのように注記されています。最も一般的にオーバーライドされるメソッドの一部には、以下が含まれます。

  • write_error - エラーページで使用するためのHTMLを出力します。

  • on_connection_close - クライアントが切断したときに呼び出されます。アプリケーションは、このケースを検出して、それ以上の処理を停止することを選択できます。切断された接続がすぐに検出できるとは限りません。

  • get_current_user - ユーザー認証を参照してください。

  • get_user_locale - 現在のユーザーに使用するLocaleオブジェクトを返します。

  • set_default_headers - レスポンスに追加のヘッダー(カスタムのServerヘッダーなど)を設定するために使用できます。

エラー処理

ハンドラーが例外を発生させると、TornadoはRequestHandler.write_errorを呼び出してエラーページを生成します。tornado.web.HTTPErrorを使用して特定のステータスコードを生成できます。その他の例外はすべて500ステータスを返します。

デフォルトのエラーページには、デバッグモードではスタックトレースが含まれ、それ以外の場合はエラーの1行の説明(例:「500: Internal Server Error」)が含まれます。カスタムエラーページを生成するには、RequestHandler.write_error(おそらく、すべてのハンドラーで共有されるベースクラスで)をオーバーライドします。このメソッドは、writerenderなどのメソッドを介して通常通り出力できます。エラーが例外によって引き起こされた場合、exc_infoタプルがキーワード引数として渡されます(この例外はsys.exc_infoの現在の例外であるとは保証されないため、write_errorは、例えばtraceback.format_exceptionを、traceback.format_excの代わりに使う必要があります)。

write_errorの代わりに、set_statusを呼び出し、レスポンスを書き込み、戻ることで、通常ハンドラーメソッドからエラーページを生成することも可能です。単に戻るのが不便な状況で、write_errorを呼び出さずにハンドラーを終了させるために、特別な例外tornado.web.Finishを発生させることができます。

404エラーの場合、default_handler_class Application 設定を使用します。このハンドラーは、任意のHTTPメソッドで動作するように、get()のようなより具体的なメソッドの代わりに、prepareをオーバーライドする必要があります。上記のように、エラーページを生成する必要があります。HTTPError(404)を発生させてwrite_errorをオーバーライドするか、self.set_status(404)を呼び出して、prepare()でレスポンスを直接生成します。

リダイレクト

Tornadoでリクエストをリダイレクトするには、主に2つの方法があります。RequestHandler.redirectと、RedirectHandlerを使用する方法です。

RequestHandlerメソッド内でself.redirect()を使用して、ユーザーを別の場所にリダイレクトできます。また、リダイレクトが永続的であると見なされることを示すために使用できるオプションのパラメータpermanentもあります。permanentのデフォルト値はFalseであり、302 Found HTTPレスポンスコードを生成し、POSTリクエストの成功後にユーザーをリダイレクトするような場合に適切です。permanentTrueの場合、301 Moved Permanently HTTPレスポンスコードが使用され、例えば、SEOに配慮した方法でページの正規URLにリダイレクトする場合に便利です。

RedirectHandler を使うと、Application のルーティングテーブルで直接リダイレクトを設定できます。たとえば、単一の静的なリダイレクトを設定するには、次のようにします。

app = tornado.web.Application([
    url(r"/app", tornado.web.RedirectHandler,
        dict(url="http://itunes.apple.com/my-app-id")),
    ])

RedirectHandler は、正規表現による置換もサポートしています。次のルールでは、/pictures/ で始まるすべてのリクエストを、代わりに /photos/ というプレフィックスにリダイレクトします。

app = tornado.web.Application([
    url(r"/photos/(.*)", MyPhotoHandler),
    url(r"/pictures/(.*)", tornado.web.RedirectHandler,
        dict(url=r"/photos/{0}")),
    ])

RequestHandler.redirect とは異なり、RedirectHandler はデフォルトで永続的なリダイレクトを使用します。これは、ルーティングテーブルが実行時に変更されず、永続的であると想定されるためです。一方、ハンドラー内のリダイレクトは、変更される可能性のある他のロジックの結果である可能性が高いためです。RedirectHandler で一時的なリダイレクトを送信するには、RedirectHandler の初期化引数に permanent=False を追加します。

非同期ハンドラー

特定のハンドラーメソッド(prepare() や HTTP 動詞メソッド get()/post()/ など)は、コルーチンとしてオーバーライドして、ハンドラーを非同期にすることができます。

たとえば、コルーチンを使用した簡単なハンドラーを次に示します。

class MainHandler(tornado.web.RequestHandler):
    async def get(self):
        http = tornado.httpclient.AsyncHTTPClient()
        response = await http.fetch("http://friendfeed-api.com/v2/feed/bret")
        json = tornado.escape.json_decode(response.body)
        self.write("Fetched " + str(len(json["entries"])) + " entries "
                   "from the FriendFeed API")

より高度な非同期の例については、チャットのサンプルアプリケーションを参照してください。これは、ロングポーリング を使用して AJAX チャットルームを実装しています。ロングポーリングのユーザーは、クライアントが接続を閉じた後にクリーンアップするために on_connection_close() をオーバーライドすることを検討できます(ただし、そのメソッドの docstring で注意事項を確認してください)。