DjangoにDjangoミドルウェアとWSGIミドルウェアを組み込んでみた

この記事は「Django Advent Calendar 2016 - Qiita」の13日目の記事です。

 
Djangoにはミドルウェアというフレームワークがあるため、リクエスト/レスポンス処理をフックして処理を追加できます。
Middleware | Django documentation | Django

また、DjangoWSGI規格に沿っていることから、WSGIミドルウェアでも処理を追加できます。
第3回 WSGIミドルウェアの作成:WSGIとPythonでスマートなWebアプリケーション開発を|gihyo.jp … 技術評論社

 
そこで今回は、

をそれぞれ作成し、Djangoに組み込んでみます。

 
目次

 

環境

 

ミドルウェアを組み込む前のDjangoアプリ

http://localhost:8000/myapp/にアクセスすると「Hello world」を表示するDjangoアプリを用意します。

myproject/urls.py

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^myapp/', include('myapp.urls', 'my')),
]

myapp/urls.py

urlpatterns = [
    url(r'^$',
        MyAppView.as_view(),
        name='index',
    ),
]

myapp/views.py

from django.views import View
from django.http import HttpResponse

class MyAppView(View):
    def get(self, request, *args, **kwargs):
        print('called: MyAppView')
        return HttpResponse('Hello world')

 

Djangoミドルウェアの組み込み

Django1.10以降の書き方のDjangoミドルウェアを組み込む

Django1.10より、Djangoミドルウェアの書き方が変更されました。そこで今回は1.10以降のDjangoミドルウェアの作法に従って書いてみます。

 
今回は各タイミングでprint()するミドルウェアを作成しました。

myproject/django_middleware_in_project.py

class DjangoMiddlewareInProject(object):
    def __init__(self, get_response):
        self.get_response = get_response
        print('proj: one-time cofiguration')

    def __call__(self, request):
        print('proj: before request')
        response = self.get_response(request)
        print('proj: after request')
        
        return response

 
MIDDLEWAREに追加します。

myproject/settings.py

MIDDLEWARE = [
    ...
    'myproject.django_middleware_in_project.DjangoMiddlewareInProject',
]

 
runserverし、http://localhost:8000/myapp/にアクセス後のログを見ます。

# 起動直後
(env) >python manage.py runserver
...
Django version 1.10.4, using settings 'myproject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
proj: one-time cofiguration
proj: one-time cofiguration

# http://localhost:8000/myapp/にアクセス後
proj: before request
called: MyAppView
proj: after request

 
各タイミングでDjangoミドルウェアが動作していることが分かりました*1

 

複数のDjangoミドルウェアの動作順を確認する

複数Djangoミドルウェアを組み込んだ場合の動作順が気になったため、試してみます。

先ほど作成したDjangoMiddlewareInProjectに加え、それと同じように動作するミドルウェア

  • myapp/django_middleware_in_app1.py
  • myapp/django_middleware_in_app2.py

として用意します。

また、MIDDLEWAREの設定を以下とします。

MIDDLEWARE = [
    ...
    'myapp.django_middleware_in_app1.DjangoMiddlewareInApp1',
    'myproject.django_middleware_in_project.DjangoMiddlewareInProject',
    'myapp.django_middleware_in_app2.DjangoMiddlewareInApp2',
]

 
runserverし、http://localhost:8000/myapp/にアクセス後のログを見ます。

(env) >python manage.py runserver
...
app2: one-time cofiguration
proj: one-time cofiguration
app1: one-time cofiguration
app2: one-time cofiguration
proj: one-time cofiguration
app1: one-time cofiguration
app1: before request
proj: before request
app2: before request
called: MyAppView
app2: after request
proj: after request
app1: after request

 
配置場所(myproject/ or myapp/)に関係なく、組み込み順(app1 > project > app2)とは逆順(app2 > project > app1)で動作するようです。

 

WSGIミドルウェアの組み込み

wsgi_lineprofの組み込み

以下を参考に、WSGIミドルウェアwsgi_lineprofを組み込みます。
How to deploy with WSGI | Django documentation | Django

 
念のため、wsgi_lineprofのソースコードを確認すると、def __call__(self, env, start_response):がありました(このあたり)。

そのため、WSGIミドルウェアとしてDjangoに組み込めそうです。

 
まずはpipでインストールします。Windowsでも問題なくインストールできました。

(env) >pip install wsgi_lineprof
...
Successfully installed wsgi-lineprof-0.2.0

(env) >pip list
Django (1.10.4)
pip (7.1.2)
setuptools (18.2)
six (1.10.0)
wheel (0.24.0)
wsgi-lineprof (0.2.0)

 
続いて、WSGIミドルウェアを組み込みます。

myproject/wsgi.py

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = get_wsgi_application()

# 以下を追加
from wsgi_lineprof.middleware import LineProfilerMiddleware
application = LineProfilerMiddleware(application)

 
runserverしてhttp://localhost:8000/myapp/にアクセス後のログを見ます。

...
File: path\to\django_project_dir\env\lib\site-packages\django\core\handlers\wsgi.py
Name: __init__
Total time: 6.1594e-06 [sec]
  Line      Hits         Time  Code
===================================
    32                             def __init__(self, stream, limit, buf_size=64 * 1024 * 1024):
    33         1            4          self.stream = stream
    34         1            2          self.remaining = limit
    35         1            2          self.buffer = b''
    36         1            4          self.buf_size = buf_size

[**/Dec/2016 **:**:**] "GET /myapp/ HTTP/1.1" 200 11

 
wsgi_lineprofが動作しています。

 

ミドルウェアでの例外ハンドリングについて

Djangoアプリで送出した例外のハンドリングについて

以前、WSGIアプリの例外をハンドリングするWSGIミドルウェアを作りました。
WSGIアプリの例外をハンドリングするWSGIミドルウェア | Pythonで、WSGIミドルウェアを作ってみた - メモ的な思考的な

 
そこで、今回も同様のWSGIミドルウェアを組み込んでみます。

まずは例外を出すViewを作成します。

myapp/views.py

class MyExceptionView(View):
    def get(self, request, *args, **kwargs):
        print('called: MyExceptionView')
        raise AssertionError(request)

        return HttpResponse('raised exception')

myapp/urls.py

urlpatterns = [
    ...
    url(r'^exception$',
        MyExceptionView.as_view(),
        name='exception',
    )
]

 
続いて、例外をハンドリングするWSGIミドルウェアを作成します。

myporject/wsgi_middleware_exception_handling.py

class WSGIMiddlewareExceptionHandling(object):
    def __init__(self, app):
        self.app = app
    
    def __call__(self, environ, start_response):
        print("called: WSGIMiddlewareExceptionHandling")

        try:
            return self.app(environ, start_response)
        except:
            print('handled exception by WSGIMiddlewareExceptionHandling')
            start_response('500 Internal Server Error', [('Content-Type', 'text/plain')])
            return [b"raised exception."]

 
最後にDjangoに組み込みます。

myproject/wsgi.py

application = get_wsgi_application()

# 以下を追加
from .wsgi_middleware_exception_handling import WSGIMiddlewareExceptionHandling
application = WSGIMiddlewareExceptionHandling(application)

 
runserverし、http://localhost:8000/myapp/exceptionにブラウザでアクセスすると、

AssertionError at /myapp/exception
<WSGIRequest: GET '/myapp/exception'>

Djangoのエラーページが表示されました。

 
ログを見ます。

(env) D:\Sandbox\Django_WSGI_middleware_sample>python manage.py runserver
...
app2: one-time cofiguration
proj: one-time cofiguration
app1: one-time cofiguration
app2: one-time cofiguration
proj: one-time cofiguration
app1: one-time cofiguration
called: WSGIMiddlewareErrorHandling
app1: before request
proj: before request
app2: before request
called: MyExceptionView
Internal Server Error: /myapp/exception
Traceback (most recent call last):
...
AssertionError: <WSGIRequest: GET '/myapp/exception'>
app2: after request
proj: after request
app1: after request
[**/Dec/2016 **:**:**] "GET /myapp/exception HTTP/1.1" 500 75449

「WSGIMiddlewareErrorHandling」は起動しているようです。

一方、「handled exception by WSGIMiddlewareErrorHandling」が表示されていないため、Djangoアプリが送出した例外をWSGIミドルウェアではハンドリングできていないようです。

 
Djangoのドキュメントを読むと、以下に例外ハンドリングに関する記載がありました。

 
意訳すると、

とようです。

 
そこで、その挙動を確認するDjangoミドルウェアを作成してみます。

myproject/django_middleware_exception_handling.py

class DjangoMiddlewareExceptionHandling(object):
    def __init__(self, get_response):
        self.get_response = get_response
        print('django_middleware: one-time cofiguration')

    def __call__(self, request):
        print('django_middleware: before request')

        try:
            response = self.get_response(request)
            print('Django response data:{}'.format(response))
        except:
            print('handled exception by DjangoMiddlewareExceptionHandling')

        print('django_middleware: after request')
        return response

    def process_exception(self, request, exception):
        print('handled exception by process_exception in DjangoMiddlewareExceptionHandling')

 
Djangoミドルウェアとして登録します。

myproject/settings.py

MIDDLEWARE = [
    ...
    # DjangoMiddlewareExceptionHandlingのみ有効化
    # 'myapp.django_middleware_in_app1.DjangoMiddlewareInApp1',
    # 'myproject.django_middleware_in_project.DjangoMiddlewareInProject',
    # 'myapp.django_middleware_in_app2.DjangoMiddlewareInApp2',
    'myproject.django_middleware_exception_handling.DjangoMiddlewareExceptionHandling',
]

 
再度http://localhost:8000/myapp/exceptionにアクセスし、ログを見ます。

(env) >python manage.py runserver
...
django_middleware: one-time cofiguration
django_middleware: one-time cofiguration
called: WSGIMiddlewareExceptionHandling
django_middleware: before request
called: MyExceptionView

# process_exception()の動作ログ
handled exception by process_exception in DjangoMiddlewareExceptionHandling
Internal Server Error: /myapp/exception
Traceback (most recent call last):
...
AssertionError: <WSGIRequest: GET '/myapp/exception'>

# get_response()コールバックの戻り値
Django response data:<HttpResponse status_code=500, "text/html">
django_middleware: after request
[**/Dec/2016 **:**:**] "GET /myapp/exception HTTP/1.1" 500 75197

 
これらより、

  • get_response()コールバックの戻り値に、ステータスコードなどがある
  • 例外をexceptした時のログなし
  • process_exception()が動作したときのログあり

ということが分かりました。

 
Djangoアプリの例外はDjangoミドルウェアでハンドリングできそうです。

 

WSGIミドルウェアで送出した例外のハンドリングについて

DjangoミドルウェアWSGIミドルウェアの挙動をみてみます。

まずは例外を送出するWSGIミドルウェアを作成します。

wsgi_middleware_raise_exception.py

class WSGIMiddlewareRaiseException(object):
    def __init__(self, app):
        self.app = app
    
    def __call__(self, environ, start_response):
        print('called: WSGIMiddlewareRaiseException')
        raise AssertionError

        self.app(environ, start_response)

 
Djangoに組み込みます。

myproject/wsgi.py

from .wsgi_middleware_raise_exception import WSGIMiddlewareRaiseException
application = WSGIMiddlewareRaiseException(application)

from .wsgi_middleware_exception_handling import WSGIMiddlewareExceptionHandling
application = WSGIMiddlewareExceptionHandling(application)

 
runserverし、http://localhost:8000/myapp/exceptionにブラウザでアクセスすると、

raised exception.

と表示されました。

 
ログを見ます。

(env) D:\Sandbox\Django_WSGI_middleware_sample>python manage.py runserver
...
django_middleware: one-time cofiguration
django_middleware: one-time cofiguration
called: WSGIMiddlewareExceptionHandling
called: WSGIMiddlewareRaiseException
handled exception by WSGIMiddlewareExceptionHandling
[**/Dec/2016 **:**:**] "GET /myapp/exception HTTP/1.1" 500 17

 
これらより、

ということが分かりました。

 

ソースコード

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

*1:起動直後に__init__()が2回呼ばれているのが気になりますけれど、環境のせいでしょうか コメントをいただきました。django.contrib.staticfilesがWSGIアプリケーションだったため、staticfilesとmyappの2回分呼ばれていました