レスポンスオブジェクト

レスポンス情報の扱いについて

リクエスト情報が簡単に取り扱えるようになったので、次はレスポンス情報の扱い方を見直します。 各エンドポイントの処理は次のように定義していました。

@app.route('^/$', 'GET')
def hello(request, start_response):
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    return [b'Hello World']

ここで気になるのは、レスポンス情報の返し方です。 WSGIのインターフェイスでは、レスポンスステータスとレスポンスヘッダーを第2引数で受け取る start_response 関数により指定します。 これはFlaskやBottleのサンプルコードに比べると、少々面倒に感じます。 もう少し簡単に管理できるように、Response情報をラップするクラスがあると便利かもしれません。 具体的には次のように各エンドポイントの処理を記述できるようにしてみます。

@app.route('^/$', 'GET')
def hello(request):
    return Response('Hello World')

@app.route('^/user/$', 'POST')
def create_user(request):
    return Response('User is created', headers={"foo": "bar"}, status=201)

Response クラスというのを追加して、レスポンスボディやヘッダー情報、ステータスコードの番号をそこに指定できるようにしました。 クラスでラップしているので、デフォルトのヘッダー情報も継承を使って自由に変更することもできます。

ヘッダー情報とステータス情報を簡単に扱う方法

Responseクラスを実装していく前に、ヘッダー情報とステータス情報を簡単に扱う方法を詳解します。 まずはステータス情報の扱いです。 start_response の第一引数にはステータスコードを 200 OK404 Not Found のように指定しますが、 番号に対応する文字列は決まっているので、番号の指定だけで済むほうが楽なものです。 http.client モジュールの responses オブジェクトには、ステータスコードの番号に対するメッセージが格納されています。

>>> from http.client import responses
>>> responses[200]
'OK'
>>> responses[404]
'Not Found'
>>> responses[500]
'Internal Server Error'

非常に簡単に取り出すことができました。ユーザーは 200 のように番号を指定してあげるだけで、次のようにステータスコードを生成できます。

>>> from http.client import responses
>>> def get_status_code(number):
...     return f"{number} {responses[number]}"
...
>>> get_status_code(200)
'200 OK'
>>> get_status_code(400)
'400 Bad Request'

ステータス情報の管理には wsgiref.headers モジュールの中にある Header クラスが便利です。

>>> from wsgiref.headers import Headers
>>> h = Headers()
>>> h.add_header('Content-type', 'text/plain')
>>> h.add_header('Foo', 'bar')
>>> h.items()
[('Content-type', 'text/plain'), ('Foo', 'bar')]

add_header(key, value) メソッドをとおして、ヘッダー情報をセットします。 またWSGIの仕様上、ヘッダー情報をキーとバリューのタプルのリストを用意する必要がありますが、 items() メソッドはその形式でヘッダー情報を吐き出してくれます。

Responseクラスを用意して組み込む

Response クラスは次のようになります。

from http.client import responses as http_responses
from wsgiref.headers import Headers

class Response:
    default_status = 200
    default_charset = 'utf-8'
    default_content_type = 'text/html; charset=UTF-8'

    def __init__(self, body='', status=None, headers=None, charset=None):
        self._body = body
        self.status = status or self.default_status
        self.headers = Headers()
        self.charset = charset or self.default_charset

        if headers:
            for name, value in headers.items():
                self.headers.add_header(name, value)

    @property
    def status_code(self):
        return "%d %s" % (self.status, http_responses[self.status])

    @property
    def header_list(self):
        if 'Content-Type' not in self.headers:
            self.headers.add_header('Content-Type', self.default_content_type)
        return self.headers.items()

    @property
    def body(self):
        if isinstance(self._body, str):
            return [self._body.encode(self.charset)]
        return [self._body]

デフォルトのステータスコードやコンテントタイプをクラス変数にもたせておくことにしました。 ユーザーはレスポンスボディの内容を文字列で指定していますが、WSGIのインターフェイスではバイト文字列を yield するイテラブルなオブジェクトとして返さなくてはいけません。 body プロパティメソッドが適切に文字列をエンコードして返してくれます。

アプリケーションにも組み込んでみましょう。

class App:
    ...

    def __call__(self, env, start_response):
        request = Request(env)
        callback, url_vars = self.router.match(request.method, request.path)

        response = callback(request, **url_vars)
        start_response(response.status_code, response.header_list)
        return response.body

組み込みはこのように非常に簡単です。これまでとは違い start_response を各関数に渡す必要はありません。 そのかわり返ってきたレスポンスオブジェクトから、ステータス情報とヘッダー情報を取り出して呼び出して上げる必要があります。

こうするとユーザーの定義する関数は驚くほどシンプルになります。 具体的には、次のようになりました。

from .app import App, Response


app = App()


@app.route('^/$', 'GET')
def hello(request):
    return Response('Hello World')


@app.route('^/user/$', 'POST')
def create_user(request):
    return Response('User Created', status=201)


@app.route('^/user/(?P<name>\w+)$', 'GET')
def user_detail(request, name):
    return Response('Hello {name}'.format(name=name))

いかがでしょう、FlaskやBottleを使ったことのある方には随分と見慣れた形になってきたのではないでしょうか。

まとめ

ここではレスポンス情報の扱いを見直しました。 Responseクラスを追加することで随分ユーザーにとって使いやすいAPIに変えることができました。 実際にアプリケーションを作っていくにはまだまだ欲しい機能がありますが、ここまでくればまさにWebフレームワークと言えるものになってきたのではないでしょうか。

ここでは全部文字列をただ返していましたが、実際のユースケースではHTMLやJSONを返すことが多いでしょう。 その内容は次の節で扱っていきます。