この記事は「Django Advent Calendar 2016 - Qiita」の13日目の記事です。
Djangoにはミドルウェアというフレームワークがあるため、リクエスト/レスポンス処理をフックして処理を追加できます。
Middleware | Django documentation | Django
また、DjangoはWSGI規格に沿っていることから、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に加え、それと同じように動作するミドルウェアを
として用意します。
また、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)
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のドキュメントを読むと、以下に例外ハンドリングに関する記載がありました。
- process-exception() | Middleware | Django documentation | Django
- exception-handling | Middleware | Django documentation | Django
意訳すると、
- 例外情報は、Djangoミドルウェアの
get_response()
コールバックの戻り値としてセット - Djangoミドルウェアでは、try/exceptは不要
- 例外ハンドリングをしたければ、Djangoミドルウェアに
process_exception()
メソッドを実装
とようです。
そこで、その挙動を確認する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')
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_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
これらより、
- Djangoミドルウェアの
__init__()
メソッドは動作する - 上記以外のDjangoミドルウェアやDjangoアプリは動作しない
- WSGIミドルウェアの例外は、WSGIミドルウェアでハンドリングできる
ということが分かりました。
ソースコード
GitHubに上げました。
thinkAmi-sandbox/Django_WSGI_middleware-sample