responderで理解するWebサーバー #3 リクエストとレスポンス

こんにちは、Webエンジニアのjuri-tです。

今回は、リクエストとレスポンスについて扱います。前回のルーティングと合わせれば、基本的な機能は抑えた気がしますね。

JSON形式でレスポンスを返す

まずは簡単な例から。

@api.route('/api/')
async def api_response(req, resp):
   resp.media = {"status": 200, "user": {"id": 1, "name": "juri-t"}}

基本はこの形です。resp.mediaにPythonのDictをセットするだけです。簡単ですね。レスポンスは以下のようになります。

{
   "status": 200,
   "user": {
       "id": 1,
       "name": "juri-t"
   }
}

YAML形式でレスポンスを返す

サーバー側は同じです。リクエストを送るクライアントが、リクエストヘッダーにAccept: application/x-yamlをセットすると、YAML形式に変わります。

status: 200
user:
 id: 1
 name: juri-t

HTML(jinja2テンプレート)を返す

APIインスタンスがtemplateメソッドを持っているので、それを呼ぶことでテンプレートを返すことができます(内部ではjinja2に委譲しています)。このとき、デフォルトの設定ではtemplatesフォルダ内を参照するので、下記の例ではtemplates/index.htmlがレンダリングされます。

@api.route('/api/')
async def api_response(req, resp):
    resp.content = api.template('index.html')

テンプレートに変数を埋め込むときの説明はいっぱいあるのでググってください。templatesフォルダじゃないところに保存したいときは、以下のように設定できます。以下ではpublic/index.htmlが対象になります。

import responder

api = responder.API(templates_dir='public')

@api.route('/api/')
async def api_response(req, resp):
    resp.content = api.template('index.html')

mediaとcontentどちらも設定したときはcontentが優先されるようです。同じエンドポイントでリクエストによって返すデータを変更したい場合は、リクエストヘッダーにAcceptを入れてもらい、以下のようにすれば良いでしょう。(req.headersで取得できます)

@api.route('/api/')
async def api_response(req, resp):
   if req.headers.get('Accept') in ['application/json', 'application/x-yaml']:
       resp.media = {"status": 200, "user": {"id": 1, "name": "juri-t"}}
   else:
       resp.content = api.template('index.html')

Requestインスタンスは、acceptsメソッドを持っているので以下のようにも書けます。というよりは、こちらのほうが良さそうです。(Acceptに複数設定されている場合もあると思うので)

@api.route('/api/')
async def api_response(req, resp):
   if req.accepts('application/json') or req.accepts('application/x-yaml'):
       resp.media = {"status": 200, "user": {"id": 1, "name": "juri-t"}}
   else:
       resp.content = api.template('index.html')

プレーンテキストを返す

@api.route('/api/')
async def api_response(req, resp):
    resp.text = 'Hello, World!'

他にもプレーンテキストを返すこともできます。

ステータスコードを変更する

レスポンスステータスコードはデフォルトが200になってます。変える場合は、resp.status_codeにセットします。

@api.route('/no_content/')
async def no_content(req, resp):
   resp.status_code = 204

ステータスコードは以下で定数化されているので、HTTP_{code}のような形で書くこともできます。

@api.route('/no_content/')
async def no_content(req, resp):
   resp.status_code = api.status_codes.HTTP_204

リダイレクトする

@api.route('/source/')
async def source(req, resp):
   api.redirect(resp, '/dest/')

@api.route('/dest/')
async def dest(req, resp):
   resp.media = {'status': 200, 'message': 'ok'}

この場合、/source/にアクセスすると、リダイレクトされて/dest/に飛びます。

レスポンスヘッダーを設定する

@api.route('/api/')
async def api_response(req, resp):
    resp.headers['Cache-Control'] = 'no-store no-cache'
    resp.content = api.template('index.html')

レスポンスヘッダーもresp.headersにそのまま入れれば良いです。

Cookieを読み書きする

@api.route('/api/')
async def api_response(req, resp):
    resp.cookies['counter'] = 1 + req.cookies['counter']
    resp.content = api.template('index.html')

cookiesで読み書き可能です。また、Cookieの属性を設定したい場合は、以下も使えます。

resp.set_cookie("hello", value="world", max_age=60)

ほかの使える属性はこちらから確認できます。

Sessionを読み書きする

import responder

api = responder.API(secret_key=os.environ['SECRET_KEY'])

@api.route('/api/')
async def api_response(req, resp):
    resp.session['counter'] = 1 + req.session['counter']
    resp.content = api.template('index.html')

sessionも同様ですね。公式ドキュメントにも書いてますが、本番環境でSessionを使う場合は事前にsecret_keyを設定しましょう。

sessionとcookieについて違いが曖昧な方は、よろしければこちらもご覧ください。


リクエストのパラメータとか

リクエストオブジェクトには他にもメソッドがあります。まとめて3つほど見てみましょう。下記のようなレスポンスをみてみます。

import responder

api = responder.API()

@api.route('/book/{id}')
async def book(req, resp, *, id):
   resp.media = {
       "full_url": req.full_url,
       "url": req.url,
       "params": req.params
   }

if __name__ == '__main__':
   api.run()

このとき、http://localhost:5042/book/101?lang=ja&author=juri-t#fragment にリクエストを送ってみると、以下のようなレスポンスを得ることができます(URLに意味はないです)。

{
   "full_url": "http://localhost:5042/book/101?lang=ja&author=juri-t",
   "url": [
       "http",
       null,
       "localhost",
       5042,
       "/book/101",
       "lang=ja&author=juri-t",
       null
   ],
   "params": {
       "lang": "ja",
       "author": "juri-t"
   }
}

req.full_urlは受け取ったURLを、req.urlは、req.full_urlをrfc3986.urlparseした配列、req.paramsはパラメータが辞書として取得できるようです。これなら簡単にハンドリングできますね(このあたりはどのフレームワークも大差ないでしょうが)。full_urlはfragmentが削除されるみたいですね。urlの2番目、7番目はnullだけど、何が入るんでしょう。ちょっと調べきれなかったので、気が向いたら調べて追記します。

というわけで、リクエストとレスポンスの扱いについて見てきました。ほとんど公式ドキュメントのままで、私が補足したのがほんの少しなので物足りない感ありますが、あまり補足することもないぐらいシンプルなAPIになっている気がします。

ではでは、今回はこのへんで〜。

サポートありがとうございます。頂いたご支援は美味しいものを食べに行きます。