WSGIアプリケーションを作る中で、WSGIミドルウェアの存在を知りました。
第3回 WSGIミドルウェアの作成:WSGIとPythonでスマートなWebアプリケーション開発を|gihyo.jp … 技術評論社
そこで今回、WSGIミドルウェアを作ってみることにしました。
目次
- 環境
- ターミナルに出力するだけのWSGIミドルウェア
- 複数のWSGIミドルウェアをWSGIアプリに載せる場合
- レスポンスを返すWSGIミドルウェア
- レスポンスを返すのはWSGIミドルウェアだけの場合
- HTTPヘッダを追加するWSGIミドルウェア
- WSGIアプリの例外をハンドリングするWSGIミドルウェア
- 複数WSGIミドルウェアがあった時の例外の伝播
- WSGIミドルウェアで例外が出た場合
- ソースコード
環境
- Windows10
- Python3.5.2
ターミナルに出力するだけのWSGIミドルウェア
関数ベースのWSGIアプリの場合
# func_with_wsgi_middleware.py from wsgiref.simple_server import make_server # WSGI Middleware class Middleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): print("Middleware") return self.app(environ, start_response) # WSGI app def hello_app(environ, start_response): print("WSGI app") start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, middleware."] # Middlewareの追加 application = Middleware(hello_app) if __name__ == "__main__": # Middlewareが含まれるWSGIアプリを起動 httpd = make_server('', 8000, application) print("Serving on port 8000...") httpd.serve_forever()
実行すると、
$ python func_with_wsgi_middleware.py Middleware WSGI app
がターミナルに表示されました。WSGIミドルウェアが動作しているようです。
クラスベースのWSGIアプリの場合
# class_with_wsgi_middleware.py # ミドルウェアは同じ class WsgiApp(object): def __call__(self, environ, start_response): print("WSGI app") start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, class with middleware."] # Middlewareを追加 application = WsgiApp() application = Middleware(application)
で関数ベースと同じことができます。
複数のWSGIミドルウェアをWSGIアプリに載せる場合
# multi_middleware.py class Middleware1(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): print("Middleware1") return self.app(environ, start_response) class Middleware2(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): print("Middleware2") return self.app(environ, start_response) class Middleware3(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): print("Middleware3") return self.app(environ, start_response) class WsgiApp(object): def __call__(self, environ, start_response): print("WSGI app") start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, class with middleware."] application = WsgiApp() application = Middleware1(application) application = Middleware2(application) application = Middleware3(application)
実行すると、
$ python multi_middleware.py Middleware3 Middleware2 Middleware1 WSGI app
と、追加したのとは逆順でWSGIミドルウェアが動作しました。
レスポンスを返すWSGIミドルウェア
# middleware_response.py # WSGI Middleware class Middleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): if environ['QUERY_STRING']: start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"respond by middleware."] return self.app(environ, start_response) # WSGIアプリは今までのものと同様 application = WsgiApp() application = Middleware(application)
とすると、
- クエリ文字列が無い場合、ブラウザにrespond by app.と表示
- クエリ文字列がある場合、ブラウザにrespond by middleware.と表示
となります。
レスポンスを返すのはWSGIミドルウェアだけの場合
WSGIアプリとWSGIミドルウェアがあり、両方実行されるがレスポンスはWSGIミドルウェアのみというケースを考えます。
最初、
# only_middleware_response_without_app.py class Middleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): print("middle ware") start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"respond by middleware."] class WsgiApp(object): def __call__(self, environ, start_response): print("wsgi app") application = WsgiApp() applicatoin = Middleware(application)
としてみましたが、実行してみると
$ python only_middleware_response_without_app.py middle ware
と、"wsgi app"はprintされませんでした。
WSGIアプリを実行するには、
# only_middleware_response_with_app.py class Middleware(object): def __call__(self, environ, start_response): print("middle ware") # WSGI appの処理をしたいときは、レスポンスを返す前に呼び出す self.app(environ, start_response) start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"respond by middleware."]
と、レスポンスを返す前にWSGIアプリを呼び出す必要があります。
HTTPヘッダを追加するWSGIミドルウェア
HTTPヘッダを追加するWSGIミドルウェアが欲しかったため調べてみたところ、stackoverflowに情報がありました。
python - How to add http headers in WSGI middleware? - Stack Overflow
# add_header_middleware.py class Middleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): # __call__()メソッドの中に、start_responseをカスタマイズする関数を用意する # また、これは関数なので、第一引数にはselfを設定しないこと def start_response_with_cookie(status_code, headers, exc_info=None): print("custom start_response") headers.append(('Set-Cookie', "hoge=fuga")) return start_response(status_code, headers, exc_info) print("Middleware") return self.app(environ, start_response_with_cookie)
と、ミドルウェアの__call__()
メソッドの中にネストして、カスタマイズしたstart_response()
関数を用意します。
実行すると、
$ python add_header_middleware.py Middleware WSGI app custom start_response
となり、レスポンスヘッダにも以下が出力されていました。
Content-Type: text/plain Set-Cookie: hoge=fuga
WSGIアプリの例外をハンドリングするWSGIミドルウェア
# exception_from_app.py class Middleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): try: print("Middleware") return self.app(environ, start_response) except Exception: print("raised exception") start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"raised exception."] class WsgiApp(object): def __call__(self, environ, start_response): print("app") raise Exception application = WsgiApp() application = Middleware(application)
としたところ、
$ python exception_from_app.py Middleware app raised exception
となり、ブラウザには「raised exception.」と表示されました。
複数WSGIミドルウェアがあった時の例外の伝播
WSGIアプリで例外が発生した時に、複数WSGIミドルウェアがあった場合はどうなるかを調べるため、
class Middleware1(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): try: print("Middleware1") return self.app(environ, start_response) except Exception: print("catch middleware1") raise class Middleware2(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): try: print("Middleware2") return self.app(environ, start_response) except Exception: print("catch middleware2") raise class Middleware3(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): try: print("Middleware3") return self.app(environ, start_response) except Exception: print("catch middleware3") start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"catch exception middleware3."] application = WsgiApp() application = Middleware1(application) application = Middleware2(application) application = Middleware3(application)
としたところ、ブラウザに「raised exception.」と表示されました。
ターミナルには
$ python exception_from_app_with_multi_middleware.py Middleware3 Middleware2 Middleware1 app catch middleware1 catch middleware2 catch middleware3
となりました。
アプリの例外では、複数WSGIミドルウェアをさかのぼるようです。
WSGIミドルウェアで例外が出た場合
class Middleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): print("Middleware") raise Exception class WsgiApp(object): def __call__(self, environ, start_response): try: print("WSGI app") start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, class with middleware."] except: print("exception")
としたところ、ブラウザに「A server error occurred. Please contact the administrator.」と表示されました。
ターミナルには
$ python exception_from_middleware.py Middleware Traceback (most recent call last): #... File "exception_from_middleware.py", line 12, in __call__ raise Exception Exception
と表示されました。
まだWSGIアプリが呼び出されていないため、WSGIアプリでの例外捕捉はできないようです。