responderで理解するWebサーバー #2 ルーティング
こんにちは、Webエンジニアのjuri-tです。
responder啓蒙活動第2回です。今回はresponderにおけるルーティングについて書きます。
READMEにも書いてある簡単な例
import responder
api = responder.API()
@api.route("/hello")
async def hello(req, resp):
resp.text = 'Hello'
if __name__ == '__main__':
api.run()
ここの、@api.routeというのがルーティングの設定になります。この場合、/helloにアクセスした際、ここでデコレートされているhello関数が呼ばれて、レスポンスにHelloが返ります。(手元で起動している場合は、http://localhost:5042/helloです)
このままだと全然面白くないので、もう少し実戦っぽい例にしましょう。
RESTful APIを実現するルーティング
import responder
api = responder.API()
@api.route("/user/{id}")
async def user(req, resp, *, id):
resp.text = f'Hello, {id}'
if __name__ == '__main__':
api.run()
RESTful APIに準拠して、特定のidのユーザーを取得するルーティングです。{id}で囲った部分が動的になります。/user/1とアクセスするとidに1になります。idに制限をかけているわけではないので、/user/juri-tとするとidにjuri-tが入ります。制限する方法は後述。
次は、リクエストメソッドに応じて処理を分岐する場合を考えましょう。
リクエストメソッドごとの処理の書き方
import responder
api = responder.API()
@api.route("/user/{id}")
async def user(req, resp, *, id):
if req.method == 'get':
resp.text = f'GET {id}'
elif req.method == 'post':
resp.text = f'POST {id}'
else:
resp.text = f'{req.method} {id}'
if __name__ == '__main__':
api.run()
Requestオブジェクトには、methodという変数にリクエストメソッドが入っています。GETの場合はgetが、POSTの場合はpostが、DELETEの場合はdeleteが、という風に取得できるので条件文で分岐すれば良いです。
さて、responderは関数ではなくクラスによるルーティングも可能です。
クラスを使ったルーティング
import responder
api = responder.API()
@api.route("/user/{id}")
class User:
async def on_get(self, req, resp, *, id):
resp.text = f'GET {id}'
async def on_post(self, req, resp, *, id):
resp.text = f'POST {id}'
async def on_request(self, req, resp, *, id):
resp.text = f'{req.method} {id}'
if __name__ == '__main__':
api.run()
この書き方でも上記と同様の処理になります。on_getでGETメソッド時の処理、on_postでPOSTメソッド時の処理です。すべてのメソッドを受け取るのはon_requestメソッドです。(ここに書いてないですが、他のメソッドもあります。たとえば、on_deleteはDELETEメソッド時の処理になります。)
さて、これでも良いですが、いろんなルーティングを用意していくと、このファイル肥大化してちょっと嫌ですね。ルーティング処理と、コントローラーの処理が混ざっているので、複雑になるとこのままだと辛いです。
コントローラーの実装を別ファイルに切り出す
controllersディレクトリを作成し、その中にusers.pyファイルを以下のように作ります。(クラスの実装は先ほどと同じです)
class User:
async def on_get(self, req, resp, *, id):
resp.text = f'GET {id}'
async def on_post(self, req, resp, *, id):
resp.text = f'POST {id}'
async def on_request(self, req, resp, *, id):
resp.text = f'{req.method} {id}'
ルーティングをするファイルはこうなります。
import responder
from controllers.users import User
api = responder.API()
api.add_route("/user/{id}", User)
if __name__ == '__main__':
api.run()
デコレータじゃなく、add_routeというメソッドでルーティングをすることもできます。これなら、ルーティングが増えても役割分担できているので大丈夫そうですね。ちなみにadd_routeにはクラスの代わりにメソッドを渡しても大丈夫です。(この記事の最初にあるhelloメソッド)
パスパラメータに型をつける
import responder
from controllers.users import User
api = responder.API()
api.add_route("/user/{id:int}", User)
if __name__ == '__main__':
api.run()
さて、上記のルーティングの処理を上のように変えてみましょう。変更点は{id:int}です。これはPythonのtype hintと呼ばれるもので、型情報を与えるものです。このとき、idに数字を与えれば正常に処理されますが、文字列を与えると404 Not Foundとなります。
404 Not Foundのときをカスタマイズしたい
ルートが見つからなかったときの404エラーのハンドリング方法ですが、現在はすでに言及されている方もいらっしゃいますが、良い方法が提供されていないようです。
ただ、エラーになるわけではなく、デフォルトのレスポンスとしてNot Foundが返ります(ちょっと味気ないので、カスタマイズできると良いんですけどね)。500も同様です。
定義しているメソッド以外を処理するルートとか、リクエストを処理する前に自前でルーティングしちゃえば良いんじゃ?とか思ったんですが、404のときはそもそもbefore_requestの処理に入らないので、それも厳しいです。
というわけで、イレギュラーなハックを考えました。自己責任でお願いします。
import responder
from controllers.users import User
api = responder.API()
api.add_route("/", None, static=True)
api.add_route("/user/{id:int}", User)
if __name__ == '__main__':
api.run()
api.add_route("/", None, static=True) この一行を加えます。このとき、staticフォルダにあるindex.htmlがデフォルトのhtmlとしてルーティングされます。
2つ目の引数はendpointとなるクラスやメソッドですが、ここはNoneである必要があります。
ただ、この方法はContent-Typeをapplication/jsonとかにしてもindex.htmlがルーティングされるし、なんといってもレスポンスのステータスが200になります。
もしかしたら2つ目の引数でルーティングするクラスをうまく作ればいけるかも?(引数が合わないとかでエラーになったのですぐにはできず)
追記(2019.7.17)
import responder
from controllers.users import User
api = responder.API()
def not_found_error(req, resp):
resp.status_code = 404
if req.headers['Content-Type'] == 'application/json':
resp.media = {"status": 404, "message": "Not Found"}
else:
resp.content = api.template('404_not_found.html')
api.add_route("/", not_found_error, default=True)
api.add_route("/user/{id:int}", User)
if __name__ == '__main__':
api.run()
こんな感じでルーティングすれば、404もカスタマイズできますね。500のサーバーエラーはすべてのルーティングに書く必要あるのかな?
そして、Content-Typeはリクエストボディの形式ですね。。
というわけで、今日はこのへんで。
いいなと思ったら応援しよう!
![juri-t](https://assets.st-note.com/production/uploads/images/12798259/profile_e18bcd31da476ac1a83c8044d36983dd.png?width=600&crop=1:1,smart)