Flaskでも使われているライブラリ Werkzeug
でいろいろと試した時のメモです。
公式ドキュメントはこちらです。以下のサンプルでも公式ドキュメントへのリンクを記載しています。
http://werkzeug.pocoo.org/docs/0.14/
なお、curlでの動作検証時、以下が参考になりました。
curl コマンド 使い方メモ - Qiita
目次
環境
- Python 3.6.6
- Werkzeug 0.14.1
Werkzeugを使ったアプリの構成
今回作成するWerkzeugを使ったアプリは、Werkzeugのチュートリアル Step 2: The Base Structure をベースに、 app.py
というファイルに追加していきます。
http://werkzeug.pocoo.org/docs/0.14/tutorial/#step-2-the-base-structure
import os from werkzeug.wrappers import Request, Response from werkzeug.wsgi import SharedDataMiddleware class BaseStructure: """Werkzeugのチュートリアルにあった、Werkzeugアプリの基本的な構成 http://werkzeug.pocoo.org/docs/0.14/tutorial/#step-2-the-base-structure """ def dispatch_request(self, request): return Response('Hello world!') def wsgi_app(self, environ, start_response): request = Request(environ) response = self.dispatch_request(request) return response(environ, start_response) def __call__(self, environ, start_response): """WSGIアプリを直接dispatchすることで、wsgi_app()をWSGIミドルウェアっぽく使える""" return self.wsgi_app(environ, start_response) def create_app(with_static=True): application = BaseStructure() if with_static: application.wsgi_app = SharedDataMiddleware( application.wsgi_app, {'/static': os.path.join(os.path.dirname(__file__), 'static')} ) return application if __name__ == '__main__': from werkzeug.serving import run_simple app = create_app() run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)
チュートリアルでは、dispatch_request() にルーティングを記載していました。
ただ、今回はルーティングについてはふれないものの、 dispatch_request() にいろいろと実装してきます。
HTTPリクエスト
WSGIアプリケーションの引数 environ
を werkzeug.wrappers.Request
に渡してインスタンス化することで、HTTPリクエストデータが扱いやすくなります。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest
request = Request(environ)
HTTPメソッド
request.method
で取得します。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.method
body.append(f'HTTP method: {request.method}') # => GET
HTTPヘッダ
request.headers
で取得します。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.headers
for k, v in request.headers.items(): body.append(f'Request header: key:{k} / value: {v}') # => Request header: key:Host / value: localhost:5000 ...
クライアントのIPアドレスなど
request.access_route
や request.remote_addr
で取得します。
- http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.access_route
- http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.remote_addr
body.append(f'access_route: {request.access_route}') # => access_route: ImmutableList(['127.0.0.1']) body.append(f'remote_addr: {request.remote_addr}') # => remote_addr: 127.0.0.1
クエリストリングの取得
request.args
や request.values
で取得します。
- http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.args
- http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.values
POSTの時にもクエリストリングの値を取得できます。
# request.argsを使う場合 body.append(f'Query String: {request.args}') # => [GET] $ curl http://localhost:5000?foo=bar の場合 # ImmutableMultiDict([('foo', 'bar')]) # => [POST] $ curl -w '\n' -X POST 'localhost:5000/?ham=spam' --data 'foo=1&bar=2' の場合 # ImmutableMultiDict([('ham', 'spam')]) # request.valuesを使う場合 body.append(f'request.values: {request.values}') # => [GET] $ curl http://localhost:5000?foo=bar の場合 # CombinedMultiDict([ImmutableMultiDict([('foo', 'bar')]), # ImmutableMultiDict([])
なお、POSTの時にもクエリストリングがあるのかどうかは、RFC的な規定は見つけられませんでした。ご存知の方は教えていただけますとありがたいです。
参考:http - Is it valid to combine a form POST with a query string? - Stack Overflow
POSTデータの取得
request.form
や request.values
で取得します。
- http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.form
- http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.values
なお、GETの時は、request.formは ImmutableMultiDict([])
になるようです。
また、POSTの場合でクエリストリング & POSTデータがある場合は、CombinedMultiDict内のImmutableMultiDictに、両方のエントリが含まれます。
順番は、クエリストリング > POSTデータ、で良さそうです。
# request.formを使う場合 body.append(f'Form: {request.form}') # => [GET] $ curl http://localhost:5000?foo=bar の場合 # ImmutableMultiDict([]) # => [POST] $ curl -w '\n' -X POST 'localhost:5000/?ham=spam' --data 'foo=1&bar=2' の場合 # ImmutableMultiDict([('foo', '1'), ('bar', '2')]) # request.valuesを使う場合 body.append(f'request.values: {request.values}') # => [POST] $ curl -w '\n' -X POST 'localhost:5000/?ham=spam' --data 'foo=1&bar=2' の場合 # CombinedMultiDict([ImmutableMultiDict([('ham', 'spam')]), # ImmutableMultiDict([('foo', '1'), ('bar', '2')]) # ])
WSGI/CGI環境変数の取得
request.environ
で取得します。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.environ
結果は dict 型であり、この中に WSGI/CGI の両方の環境変数が含まれるようです。
body.append(f'environ: {type(request.environ)} / {request.environ}') # => environ: <class 'dict'> / {'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': <_io.BufferedReader name=6>, 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'werkzeug.server.shutdown': <function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x105f13c80>, 'SERVER_SOFTWARE': 'Werkzeug/0.14.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'PATH_INFO': '/', 'QUERY_STRING': 'foo=bar', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': 55330, 'SERVER_NAME': '0.0.0.0', 'SERVER_PORT': '5000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_HOST': 'localhost:5000', 'HTTP_CONNECTION': 'keep-alive', 'HTTP_PRAGMA': 'no-cache', 'HTTP_CACHE_CONTROL': 'no-cache', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'ja,en-US;q=0.9,en;q=0.8', 'HTTP_COOKIE': 'one_time=x; counter=8', 'werkzeug.request': <Request 'http://localhost:5000/?foo=bar' [GET]>}
HTTPレスポンス
werkzeug.wrappers.Response
を使うことで、HTTPレスポンスデータとなります。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseResponse
# msgは、HTTPレスポンスボディ
response = Response(msg)
Content-Typeをセット
response.content_type
を使います。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.CommonRequestDescriptorsMixin.content_type
response.content_type = 'application/json'
独自HTTPヘッダをセット
独自HTTPヘッダをセットするには
response.headers.add
response.headers.add_header
response.headers['key'] = 'value'
の3つが使えます。
response.headers.add('X-headers-add', 'using add') response.headers.add_header('X-headers-add_header', 'using add_header') response.headers['X-headers-key'] = 'using key' # => X-headers-add: using add # X-headers-add_header: using add_header # X-headers-key: using key
エラーページを表示
abort()
や raise InternalServerError()
などが使えます。
- http://werkzeug.pocoo.org/docs/0.14/exceptions/#usage-example
- http://werkzeug.pocoo.org/docs/0.14/exceptions/#simple-aborting
なお、例外クラスを継承し、エラーページを差し替えることもできます。
http://werkzeug.pocoo.org/docs/0.14/exceptions/#custom-errors
以下の例では、InternalServerErrorを継承し、HTTP500エラー時の画面の表示を差し替えています。
class MyInternalServerError(InternalServerError): def get_body(self, environ=None): # werkzeug._compat.text_type()がprotectedなので、使っていいものか... return text_type( u'<!DOCTYPE html>' u'<title>My Internal Server Error</title>' u'<h1>Oh, my internal server error!</h1>' )
リダイレクト
werkzeug.utils.redirect()
が使えます。
http://werkzeug.pocoo.org/docs/0.14/utils/#werkzeug.utils.redirect
return redirect('https://www.google.co.jp')
なお、URLに /
を追加するリダイレクトのための werkzeug.utils.append_slash_redirect
も用意されています。
http://werkzeug.pocoo.org/docs/0.14/utils/#werkzeug.utils.append_slash_redirect
Cookie
Werkzeugではリクエスト時のCookieとレスポンス時のCookieが分かれています。
リクエストCookieの取得
request.cookies
ですべてのCookieを取得します。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.cookies
Cookieのキーを指定して値を取得するには、以下となります。
counter = request.cookies.get('counter', 0)
レスポンスCookieの設定
レスポンスオブジェクトを生成した後、 response.set_cookie()
にてCookieをセットします。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseResponse.set_cookie
response = Response(msg) response.set_cookie('counter', str(int(counter) + 1))
なお、 response.delete_cookie()
でCookieを削除できます。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseResponse.delete_cookie
delete_cookie() を呼ぶと
Set-Cookie: one_time=; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/
のようなHTTPレスポンスヘッダが追加されます。
Cookieを削除しない限り、リクエストオブジェクトからレスポンスオブジェクトにCookieを移し替えなくても、次回のリクエスト時も同じキー・値でCookieがやってきます。
同じCookieキー名での設定に注意
例えば、
response.set_cookie('same_cookie', '1st', httponly=True) response.set_cookie('same_cookie', '2nd', secure=True)
のように、同じCookieキー名 (same_cookie) でCookieをセットしたとします。
この場合、HTTPレスポンスヘッダは
Set-Cookie: same_cookie=1st; HttpOnly; Path=/ Set-Cookie: same_cookie=2nd; Secure; Path=/
となります。
ただし、リロードするなどして再度アクセスすると、HTTPリクエスト時のCookieヘッダは
Cookie: one_time=x; same_cookie=1st; counter=2
と、片方のものしかセットされていません。
CookieのRFC6265を見ると、
(原文)
Servers SHOULD NOT include more than one Set-Cookie header field in the same response with the same cookie-name. (See Section 5.2 for how user agents handle this case.)
(日本語訳)
サーバは、同じ応答内に同じ cookie-name の複数の Set-Cookie ヘッダを内包するべきでない。 (UA がこの場合をどう扱うかについては、 5.2 節を見よ。)
https://triple-underscore.github.io/http-cookie-ja.html#section-4.1
とあります。
これらより、同じCookieキー名を設定すべきではなく、挙動もおかしくなるため、注意が必要です。
その他
開発WSGIサーバを外部アクセス可能にする
werkzeug.serving.run_simple()
で開発WSGIサーバを起動できます。
http://werkzeug.pocoo.org/docs/0.14/serving/#werkzeug.serving.run_simple
なお、Flask同様、引数hostに 0.0.0.0
を指定することで、開発WSGIサーバを外部アクセス可能にします。
Flaskのサーバーはデフォルトだと公開されてない - Qiita
favicon.ico のリクエストを処理する
counter = request.cookies.get('counter', 0) response.set_cookie('counter', str(int(counter) + 1))
と実装し、ブラウザで表示した後、リロードしたところ、HTTPレスポンスヘッダが
Set-Cookie: counter=3; Path=/
となっていました。counter=2を想定していたのに、counter=3となっていました。
原因は、ブラウザでアクセスした時、1回で
の2リクエストが飛んでいたせいでした。
今回のアプリはルーティングがないため、favicon.icoのリクエストでも dispatch_request() が処理されてしまったようです。
対応としては、開発環境の場合は werkzeug.wsgi.SharedDataMiddleware
が使えます *1 。
create_app()関数の中で、SharedDataMiddlewareを使います。
なお、Werkzeugのチュートリアルなどではディレクトリを指定していますが、以下のようにファイル自体を指定することも可能です。
def create_app(with_static=True): application = Application() # WSGIミドルウェアの設定ポイント if with_static: application.wsgi_app = SharedDataMiddleware( application.wsgi_app, {'/favicon.ico': str(pathlib.Path('./favicon.ico'))} ) return application
次に、 app.py と同じディレクトリの中に favicon.ico を用意します。
以上で、favicon.icoへのアクセスはSharedDataMiddlewareにより処理され、dispatch_request()では処理されなくなります。
ソースコード
GitHubに上げました。base_structure
ディレクトリ以下が今回のファイルです。
thinkAmi-sandbox/werkzeug-sample
*1:本番環境では何らかの形で静的ファイルを配信するはず