Pythonで、WSGIミドルウェアを作ってみた

WSGIアプリケーションを作る中で、WSGIミドルウェアの存在を知りました。
第3回 WSGIミドルウェアの作成:WSGIとPythonでスマートなWebアプリケーション開発を|gihyo.jp … 技術評論社

 
そこで今回、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アプリでの例外捕捉はできないようです。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/wsgi_middleware_sample