1. Django を WSGI 視点で見る
第1部では TCP ソケットから WSGI の仕様、そしてフレームワークが WSGI の上に何を積み上げているかを学びました。
第2部の最初となる本章では、Django を「WSGI アプリケーションとして」読み解きます。
普段 python manage.py runserver で起動している Django が、実は Vol.1「WSGI が生まれた背景」で書いた application(environ, start_response) とまったく同じインタフェースで動いていることを確認するところから始めましょう。
1.1. wsgi.py の役割
Django プロジェクトを django-admin startproject myproject で生成すると、myproject/wsgi.py というファイルが自動的に作られます。
# 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()
このファイルはわずか4行ですが、WSGI サーバと Django を接続する唯一の入り口です。
Gunicorn で Django を起動するとき gunicorn myproject.wsgi:application と指定しますが、これは「myproject/wsgi.py モジュールの中にある application という名前の WSGI callable を使え」という意味です。
wsgi.py が担っている責務は次の二つです。
設定ファイルの宣言:
DJANGO_SETTINGS_MODULE環境変数を設定することで、Django がどの設定ファイルを読み込むべきかを宣言します。この環境変数が未設定のまま Django を起動するとImproperlyConfigured例外が発生します。WSGI callable の生成:
get_wsgi_application()を呼び出して WSGI callable を生成し、モジュールレベル変数applicationに束縛します。
重要
application は Vol.1「WSGI が生まれた背景」で学んだ WSGI の仕様に完全に準拠した callable です。
つまり application(environ, start_response) の形で呼び出され、バイト列のイテラブルを返します。
Django のモデル、テンプレート、ORM、管理画面、認証といった膨大な機能は、すべてこの一つの callable の内側に収まっています。
1.2. get_wsgi_application()
get_wsgi_application() の内部は驚くほどシンプルです。
Django のソースコード(django/core/wsgi.py)を確認すると、本質的には以下の処理しか行っていません。
# django/core/wsgi.py(簡略化)
import django
from django.core.handlers.wsgi import WSGIHandler
def get_wsgi_application():
django.setup()
return WSGIHandler()
django.setup() は Django の初期化処理を実行します。
具体的には、次の処理を順に行います。
DJANGO_SETTINGS_MODULEで指定された設定ファイルを読み込むINSTALLED_APPSに登録されたアプリケーションのAppConfigをロードするモデルのインポートやシグナルの接続といった起動時処理を完了させる
注釈
この setup() はプロセスの生存期間中に一度だけ実行されます。
Gunicorn のワーカーが fork される前にこの処理が走るため、各ワーカーは初期化済みの状態で起動します。
WSGIHandler() が返すオブジェクトこそが、Django の WSGI アプリケーション本体です。
WSGIHandler は __call__ メソッドを持つクラスで、Vol.1「WSGI が生まれた背景」で関数として書いた WSGI アプリケーションをクラスベースで実装したものです。
# django/core/handlers/wsgi.py(概念的な構造)
class WSGIHandler:
def __init__(self):
self.load_middleware()
def __call__(self, environ, start_response):
request = WSGIRequest(environ)
response = self.get_response(request)
status = f"{response.status_code} {response.reason_phrase}"
headers = list(response.items())
start_response(status, headers)
return response
この __call__ の中で起きていることは、次の三つの段階に分かれます。
environ の変換:
environ辞書をWSGIRequestオブジェクトに変換します。Vol.1「WSGI の上に何が必要になるのか」で Werkzeug のRequest(environ)が行っていた処理と同じ発想で、environ["REQUEST_METHOD"]をrequest.method、environ["PATH_INFO"]をrequest.pathといった属性アクセスに変換します。レスポンスの取得:
self.get_response(request)でミドルウェアチェーンとビュー関数を通過させ、HttpResponseオブジェクトを得ます。WSGI への引き渡し:
HttpResponseからステータスコードとヘッダーを取り出してstart_responseに渡し、レスポンスボディをイテラブルとして返します。
この構造を図示すると次のようになります。
Gunicorn (WSGI サーバ)
│
│ environ, start_response
▼
WSGIHandler.__call__()
│
├─ WSGIRequest(environ) ← environ → request オブジェクト変換
│
├─ self.get_response(request) ← ミドルウェア → URL解決 → ビュー実行
│
├─ start_response(status, headers)
│
└─ return response ← バイト列イテラブル
Vol.1「WSGI が生まれた背景」で wsgiref.simple_server を使って自作の WSGI アプリを動かしたのと同様に、Django の application も任意の WSGI サーバ上で動作します。
wsgiref でも、Gunicorn でも、uWSGI でも、application(environ, start_response) を呼び出すだけで Django の全機能が起動するのは、WSGI という共通インタフェースが存在するからです。
次節では WSGIHandler.get_response() の内部に踏み込み、Django のミドルウェアチェーンがどのように構築され、リクエストがビュー関数に届くまでにどのような処理を通過するかを追跡します。
1.3. リクエストは Django にどう渡るか
前節で WSGIHandler.__call__ が environ を受け取り、WSGIRequest に変換する流れを概観しました。
本節ではその変換処理の内部を詳しく追い、Vol.1「WSGI が生まれた背景」で学んだ environ 辞書のキーが Django の request オブジェクトのどの属性に対応するかを具体的に確認します。
1.3.1. WSGI environ から HttpRequest へ
Vol.1「WSGI が生まれた背景」で見た通り、WSGI サーバはリクエストの情報を environ 辞書に詰めてアプリケーションに渡します。
Django の WSGIRequest はこの辞書を受け取り、開発者が直感的に扱える属性へ変換します。
対応関係を整理すると以下のようになります。
environ キー |
request 属性 |
内容 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
(body 読み取りに使用) |
バイト数 |
|
|
|
|
|
ホスト名 |
|
|
|
|
|
サーバ名 |
|
|
ポート番号 |
environ 全体 |
|
|
注釈
request.META が environ そのものをほぼそのまま保持している点が重要です。
Django は environ の内容を独自のフォーマットに変換するのではなく、CGI 由来のキー名をそのまま META に格納します。
そのため、Vol.1「WSGI が生まれた背景」で environ["HTTP_X_REQUEST_ID"] として取得していたカスタムヘッダーは、Django でも request.META["HTTP_X_REQUEST_ID"] で取得できます。
Django 3.2 以降では request.headers という辞書ライクなオブジェクトが追加され、request.headers["X-Request-Id"] のように元のヘッダー名に近い形式でもアクセスできるようになりました。
内部的には request.headers が META の HTTP_ プレフィックス付きキーを逆変換しているだけで、データソースは同一です。
1.3.2. request オブジェクトの生成
WSGIRequest の生成過程を、Django のソースコード(django/core/handlers/wsgi.py および django/http/request.py)に沿って追跡します。
# django/core/handlers/wsgi.py(概念的な構造)
class WSGIRequest(HttpRequest):
def __init__(self, environ):
self.environ = environ
self.META = environ
self.method = environ["REQUEST_METHOD"].upper()
self.path_info = environ.get("PATH_INFO", "/")
self.content_type = environ.get("CONTENT_TYPE", "")
self.content_params = {} # Content-Type のパラメータ解析結果
self._read_started = False
self._stream = environ["wsgi.input"]
self._post_parse_error = False
script_name = environ.get("SCRIPT_NAME", "")
self.path = f"{script_name}{self.path_info}"
WSGIRequest は HttpRequest を継承しています。
HttpRequest: Django のリクエスト抽象基底クラスで、共通のプロパティやメソッドを定義します。WSGIRequest: WSGI 固有の処理(environからの変換)を担当します。
この分離は5 章(Django は ASGI にどう対応しているか)で登場する ASGIRequest と対比すると明確になります。
ASGIRequest も同じ HttpRequest を継承しつつ、ASGI の scope と receive からリクエスト情報を取得します。
1.3.2.1. body の遅延読み取り
request.body はプロパティとして定義されており、初回アクセス時に wsgi.input ストリームから読み取りが実行されます。
# django/http/request.py(概念的な構造)
class HttpRequest:
@property
def body(self):
if not hasattr(self, "_body"):
try:
content_length = int(self.META.get("CONTENT_LENGTH", 0) or 0)
self._body = self._stream.read(content_length)
except Exception:
self._body = b""
raise
self._stream = BytesIO(self._body)
return self._body
この実装には三つの設計上の工夫があります。
遅延読み取り(lazy loading): GET リクエストのようにボディが不要な場合は、ストリームの読み取りが一切発生しません。
再読み取りへの対応: 読み取り後に
self._streamをBytesIO(self._body)で置き換えることで、ミドルウェアやビュー関数がrequest.read()を呼んでも再度読み取り可能にしています(Vol.1「WSGI が生まれた背景」で触れた「wsgi.inputは一度しか読めない」問題への対処です)。ValueError の防止:
CONTENT_LENGTHが空文字列や未設定の場合に0をフォールバック値として使い、int()のValueErrorを防いでいます。
1.3.2.2. GET と POST の解析
クエリ文字列とフォームデータの解析も遅延実行されます。
# django/http/request.py(概念的な構造)
class HttpRequest:
@cached_property
def GET(self):
return QueryDict(self.META.get("QUERY_STRING", ""), encoding=self.encoding)
@cached_property
def POST(self):
self._load_post_and_files()
return self._post
QueryDict は Django 独自の辞書サブクラスで、同一キーの複数値をサポートします。
?color=red&color=blue のようなクエリ文字列に対して request.GET.getlist("color") で ["red", "blue"] を取得できます。
内部的には urllib.parse.parse_qs と同様の処理を行いつつ、イミュータブル(変更不可)な辞書として振る舞います。
request.POST はフォームデータ(Content-Type: application/x-www-form-urlencoded または multipart/form-data)を解析した結果です。
注意
JSON ボディは request.POST には入りません。
Content-Type: application/json で送信されたデータは json.loads(request.body) で明示的にパースする必要があります。
この設計は、Django が HTML フォーム送信を主な POST の用途として想定していた時代の名残です。
1.3.2.3. ファイルアップロード
multipart/form-data で送信されたファイルは request.FILES に格納されます。
Django は Content-Length に応じてファイルをメモリに保持するかディスクに書き出すかを FILE_UPLOAD_MAX_MEMORY_SIZE(デフォルト約2.5 MB)で制御します。
サイズ |
保持先 |
クラス |
|---|---|---|
小さいファイル |
メモリ |
|
大きいファイル |
ディスク(一時保存) |
|
1.3.2.4. 全体の流れ
リクエストオブジェクト生成の全体像をまとめると、次の順序で処理が進みます。
WSGI サーバが environ を生成
│
▼
WSGIRequest.__init__(environ)
├─ self.META = environ(参照を保持)
├─ self.method = environ["REQUEST_METHOD"]
├─ self.path = SCRIPT_NAME + PATH_INFO
├─ self._stream = environ["wsgi.input"](未読み取り)
│
▼
ミドルウェア / ビュー関数がアクセス(遅延評価)
├─ request.body → _stream.read(content_length) → キャッシュ
├─ request.GET → QueryDict(QUERY_STRING) → キャッシュ
├─ request.POST → body 解析 → キャッシュ
├─ request.FILES → multipart 解析 → キャッシュ
└─ request.headers → META の HTTP_* を逆変換
Tip
Vol.1「まずは 1 リクエストだけ処理するサーバを作る」で自作サーバに書いた parse_request_line、parse_headers、parse_qs は、Django の内部ではこのように WSGIRequest と HttpRequest のプロパティに分散して実装されています。
抽象化の層は増えていますが、やっていることの本質は変わりません。
次節では、生成された request オブジェクトがミドルウェアチェーンをどのように通過し、URL 解決を経てビュー関数に届くかを追跡します。
1.4. URL 解決の流れ
前節で WSGIRequest オブジェクトが生成されるまでを追いました。
本節では、そのリクエストが持つパス情報(request.path_info)をもとに、Django がどのビュー関数を呼び出すかを決定する「URL 解決(URL resolution)」の仕組みを内部から追跡します。
1.4.1. URLconf
Django の URL 解決はすべて、プロジェクトの ルート URLconf から始まります。
settings.py の ROOT_URLCONF で指定されたモジュール(通常は myproject/urls.py)が起点です。
# myproject/settings.py
ROOT_URLCONF = 'myproject.urls'
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/users/', include('users.urls')),
path('api/articles/', include('articles.urls')),
]
# users/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.user_list, name='user-list'),
path('<int:user_id>/', views.user_detail, name='user-detail'),
path('<int:user_id>/posts/', views.user_posts, name='user-posts'),
]
urlpatterns はリストですが、単なる文字列のリストではありません。
各 path() や include() は内部で URLPattern または URLResolver オブジェクトを生成します。
クラス |
役割 |
|---|---|
|
末端のビュー関数へのマッピング |
|
子の |
つまり urlpatterns 全体は木構造を形成しており、Django はこの木をルートから順にたどってマッチするパターンを探します。
URLResolver (root: "")
├─ URLResolver ("admin/") → admin.site.urls
├─ URLResolver ("api/users/") → users.urls
│ ├─ URLPattern ("") → views.user_list
│ ├─ URLPattern ("<int:user_id>/") → views.user_detail
│ └─ URLPattern ("<int:user_id>/posts/") → views.user_posts
└─ URLResolver ("api/articles/") → articles.urls
└─ ...
注釈
この木構造はプロセス起動時に一度だけ構築されます。以降のリクエストでは構築済みの木を探索するだけなので、include() を使ってアプリケーションごとに urls.py を分割しても、最終的には単一の木に統合されます。
1.4.2. path と re_path
path() と re_path() は URL パターンを定義する二つの方法です。
from django.urls import path, re_path
# path() — シンプルな構文、型変換付き
path('users/<int:user_id>/', views.user_detail)
# re_path() — 正規表現による柔軟なマッチング
re_path(r'^users/(?P<user_id>[0-9]+)/$', views.user_detail)
path() は Django 2.0 で導入された構文で、<型:名前> 形式のパスコンバータを使います。
Django が標準で提供するコンバータは次の5種類です。
コンバータ |
変換後の型 |
マッチ対象 |
|---|---|---|
|
|
スラッシュ以外の文字列(デフォルト) |
|
|
0 以上の整数 |
|
|
スラッグ文字列 |
|
|
UUID 形式 |
|
|
スラッシュを含む文字列 |
内部的には、各コンバータはパターンを正規表現に変換しています。
たとえば <int:user_id> は (?P<user_id>[0-9]+) に変換されます。
つまり path() は re_path() の構文糖であり、最終的に同じ正規表現マッチングエンジンで処理されます。
path() には重要な追加機能として、マッチした文字列を自動的に Python の型に変換する機能があります。
<int:user_id> でマッチした "42" は、ビュー関数に渡される前に int("42") → 42 に変換されます。
re_path() は正規表現をそのまま記述できるため、より複雑なパターン(例: 日付形式 (?P<year>[0-9]{4})/(?P<month>[0-9]{2})/)に対応できますが、型変換は行われません。
マッチ結果は常に文字列としてビュー関数に渡されます。
1.4.3. resolver match
URL 解決が実行されるタイミングは、ミドルウェアの処理中です。
Django の WSGIHandler.get_response() から呼ばれるミドルウェアチェーンの中で、request.path_info に対して URLResolver.resolve() が実行されます。
# django/urls/resolvers.py(概念的な構造)
class URLResolver:
def resolve(self, path):
for pattern in self.url_patterns:
match = pattern.match(path)
if match:
new_path, args, kwargs = match
if isinstance(pattern, URLResolver):
# 中間ノード: 残りのパスで再帰的に解決
return pattern.resolve(new_path)
else:
# 末端ノード: ビュー関数が見つかった
return ResolverMatch(
pattern.callback, # ビュー関数
args,
kwargs,
pattern.name, # URL 名
)
raise Resolver404({"path": path})
例として /api/users/42/ がリクエストされた場合の処理を追います。
ルートの URLResolver が urlpatterns を上から順に走査し、"admin/" はマッチしません。
次の "api/users/" がマッチし、残りのパス "42/" で users.urls の URLResolver に再帰します。
users.urls の中で "" はマッチせず、"<int:user_id>/" が "42/" にマッチします。
この時点で ResolverMatch オブジェクトが生成されます。
ResolverMatch(
func=views.user_detail,
args=(),
kwargs={"user_id": 42}, # int に変換済み
url_name="user-detail",
)
マッチ結果は request.resolver_match に格納されます。
どのパターンにもマッチしなかった場合は Resolver404 例外が送出され、Django はこれを捕捉して 404 レスポンスに変換します。
注釈
Vol.1「本書の対象読者とゴール」で「404 はどこで発生するか」を整理した際に挙げた「フレームワーク層での 404」がまさにこのケースです。
1.4.4. path parameter の受け渡し
ResolverMatch に格納された args と kwargs は、ビュー関数の引数としてそのまま渡されます。
# Django がビューを呼び出す箇所(概念的な構造)
resolver_match = request.resolver_match
response = resolver_match.func(request, *resolver_match.args, **resolver_match.kwargs)
つまり /api/users/42/ に対して views.user_detail(request, user_id=42) が呼び出されます。
ビュー関数側では以下のように受け取ります。
# users/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from .models import User
def user_detail(request, user_id):
# user_id は int 型で渡される(path コンバータが変換済み)
user = get_object_or_404(User, id=user_id)
return JsonResponse({
"id": user.id,
"name": user.name,
"email": user.email,
})
この受け渡しの仕組みは、Vol.1「WSGI の上に何が必要になるのか」で Werkzeug の Map/Rule がエンドポイントと values を返し、Flask がそれをビュー関数の引数に展開する処理と同じ発想です。
パスから値を抽出し、型を変換し、関数の引数にマッピングするという一連の処理を、Django は URLResolver → ResolverMatch → ビュー呼び出しの流れで実現しています。
警告
パスパラメータの名前はビュー関数の仮引数名と一致している必要があります。
<int:user_id> と定義した場合、ビュー関数は def user_detail(request, user_id) でなければなりません。
名前が一致しないと TypeError が発生し、Django はこれを 500 エラーとして処理します。
クラスベースビュー(CBV)の場合も本質は同じです。
as_view() が返す関数が呼び出され、内部で self.kwargs にパスパラメータが格納されます。
# クラスベースビューでのパスパラメータ取得
class UserDetailView(View):
def get(self, request, user_id):
# 関数ベースビューと同じく引数で受け取れる
...
# あるいは self.kwargs から取得
def get(self, request, **kwargs):
user_id = self.kwargs["user_id"]
...
URL 解決の仕組みを理解すると、urlpatterns の記述順序が重要である理由も明確になります。
Django は上から順にマッチを試みるため、より限定的なパターンを先に、より汎用的なパターンを後に配置する必要があります。
次節では、URL 解決で特定されたビュー関数が実際に呼び出される前後に介入する「ミドルウェア」の仕組みを追跡します。
1.5. middleware chain の流れ
前節で URL 解決によりビュー関数が特定されるまでを追いました。 しかし Django では、リクエストがビューに届く前にも、レスポンスがクライアントに返る前にも、複数の処理層を通過します。 それがミドルウェアです。
本節では Django のミドルウェアチェーンがどのように構築され、リクエストとレスポンスがどの順序で流れるかを内部構造から追跡します。
1.5.1. リクエスト前処理
Django の settings.py には MIDDLEWARE というリストが定義されています。
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
重要
このリストの順序は極めて重要です。 Django はサーバ起動時にこのリストを 末尾から先頭へ 逆順に処理し、各ミドルウェアを入れ子にしたチェーンを構築します。
概念的には次のような構造になります。
# django/core/handlers/base.py(概念的な構造)
class BaseHandler:
def load_middleware(self):
handler = self._get_response # 最も内側: URL解決 → ビュー呼び出し
for middleware_path in reversed(settings.MIDDLEWARE):
middleware_class = import_string(middleware_path)
handler = middleware_class(handler)
self._middleware_chain = handler
self._get_response はリクエストを受け取り、URL 解決を行い、ビュー関数を呼び出して HttpResponse を返す関数です。
これを最も内側に置き、MIDDLEWARE リストの末尾から順にラップしていきます。
結果として、リストの先頭に書かれたミドルウェアが最も外側になります。
リクエスト →
SecurityMiddleware.process_request
SessionMiddleware.process_request
CommonMiddleware.process_request
CsrfViewMiddleware.process_request
AuthenticationMiddleware.process_request
MessageMiddleware.process_request
XFrameOptionsMiddleware.process_request
─── URL解決 → ビュー関数実行 ───
XFrameOptionsMiddleware.process_response
MessageMiddleware.process_response
AuthenticationMiddleware.process_response
CsrfViewMiddleware.process_response
CommonMiddleware.process_response
SessionMiddleware.process_response
SecurityMiddleware.process_response
← レスポンス
各ミドルウェアの基本構造は以下の通りです。
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# 起動時の初期化処理(一度だけ実行)
def __call__(self, request):
# ---- リクエスト前処理 ----
# request を検査・加工できる
response = self.get_response(request)
# ---- レスポンス後処理 ----
# response を検査・加工できる
return response
self.get_response(request) を呼ぶことで、一つ内側のミドルウェア(あるいは最終的にはビュー関数)にリクエストが渡されます。
この呼び出しの前に書いたコードがリクエスト前処理、後に書いたコードがレスポンス後処理です。
リクエスト前処理の段階では、各ミドルウェアが次の処理を順に実行します。
SecurityMiddleware: HTTPS リダイレクトやStrict-Transport-Securityヘッダーの準備を行います。SessionMiddleware: Cookie からセッション ID を取得してrequest.sessionを生成します。AuthenticationMiddleware: セッション情報をもとにユーザーを特定してrequest.userを設定します。
これらは順番に依存関係を持っており、AuthenticationMiddleware が request.session を参照するためには SessionMiddleware が先に実行されている必要があります。MIDDLEWARE リストの順序が重要な理由はここにあります。
ミドルウェアはリクエスト前処理の段階で self.get_response を呼ばずに直接 HttpResponse を返すこともできます。
たとえば CsrfViewMiddleware が CSRF トークンの検証に失敗した場合、ビュー関数は一切実行されず 403 レスポンスがそのまま返却されます。
この「短絡(short-circuit)」の仕組みにより、不正なリクエストを早期に遮断できます。
1.5.2. ビュー前後処理
Django のミドルウェアには __call__ 以外にも、ビュー関数の前後に介入するフックメソッドが用意されています。
class DetailedMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
"""URL解決後、ビュー関数呼び出し前に実行される"""
# None を返すと通常通りビューが実行される
# HttpResponse を返すとビューをスキップ
return None
def process_template_response(self, request, response):
"""ビュー関数が TemplateResponse を返した場合に実行される"""
return response
process_view は URL 解決が完了し、どのビュー関数が呼ばれるかが確定した後、実際にビューが呼ばれる直前に実行されます。
引数としてビュー関数そのもの(view_func)とパスパラメータ(view_args, view_kwargs)を受け取るため、ビュー関数の種類に応じた処理が可能です。
CsrfViewMiddleware はこのフックで CSRF 検証を行い、ビュー関数に @csrf_exempt デコレータが付いている場合は検証をスキップします。
処理の順序を時系列で整理すると次のようになります。
__call__のリクエスト前処理(外側から内側へ)—SecurityMiddleware→SessionMiddleware→ … →XFrameOptionsMiddlewareURL 解決(
request.path_info→ResolverMatch)process_view(外側から内側へ)—SecurityMiddleware.process_view→ … →XFrameOptionsMiddleware.process_viewビュー関数の実行
process_template_response(内側から外側へ、TemplateResponseの場合のみ)__call__のレスポンス後処理(内側から外側へ)—XFrameOptionsMiddleware→ … →SessionMiddleware→SecurityMiddleware
1.5.3. レスポンス後処理
ビュー関数が HttpResponse を返した後、レスポンスはミドルウェアチェーンを内側から外側へ逆順に通過します。
self.get_response(request) の戻り値として受け取った response オブジェクトを、各ミドルウェアが検査・加工します。
class ExampleResponseMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# レスポンス後処理の例
response['X-Request-Id'] = generate_request_id()
return response
レスポンス後処理では、各ミドルウェアが次の処理を実行します。
XFrameOptionsMiddleware:X-Frame-Optionsヘッダーを付与してクリックジャッキング攻撃を防ぎます。CommonMiddleware:Content-Lengthヘッダーを設定して必要に応じて末尾スラッシュのリダイレクトを処理します。SessionMiddleware: セッション変更時に Cookie へセッション ID を書き込みます。SecurityMiddleware:Strict-Transport-Securityヘッダーを付与します。
ここでも順序が重要です。
SessionMiddleware がセッション Cookie を Set-Cookie ヘッダーに書き込んだ後で、SecurityMiddleware がヘッダー全体を検査します。
もし順序が逆だと、セッション Cookie の設定前にセキュリティヘッダーの処理が終了してしまいます。
1.5.4. 例外処理との関係
ビュー関数やミドルウェアの処理中に例外が発生した場合、Django は process_exception フックを通じて例外を処理します。
class ErrorTrackingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_exception(self, request, exception):
"""ビュー関数が例外を送出した場合に実行される"""
# None を返すとデフォルトの例外処理に委ねる
# HttpResponse を返すとそれがレスポンスになる
logger.error(f"Exception in {request.path}: {exception}", exc_info=True)
return None
process_exception はビュー関数が例外を送出した場合にのみ呼ばれ、ミドルウェアチェーンを 内側から外側へ 順に辿ります。
いずれかのミドルウェアが HttpResponse を返した時点で例外処理は終了し、通常のレスポンス後処理フローに戻ります。
すべてのミドルウェアが None を返した場合、Django のデフォルトの例外処理が適用されます。
Django のデフォルト例外処理は例外の種類に応じたレスポンスを生成します。
例外クラス |
HTTP ステータス |
|---|---|
|
404 Not Found |
|
403 Forbidden |
|
400 Bad Request |
その他の未処理例外 |
500 Internal Server Error |
500 エラーが発生した場合、DEBUG=True であれば詳細なスタックトレースを含むエラーページが、DEBUG=False であれば汎用的なエラーページが返されます。
例外処理の流れを図示します。
ビュー関数で ValueError が発生
│
▼
process_exception(内側 → 外側)
XFrameOptionsMiddleware.process_exception → None(処理しない)
MessageMiddleware.process_exception → None
AuthenticationMiddleware.process_exception → None
CsrfViewMiddleware.process_exception → None
CommonMiddleware.process_exception → None
SessionMiddleware.process_exception → None
SecurityMiddleware.process_exception → None
│
▼(すべて None を返した)
Django デフォルト例外処理
→ DEBUG=True: 詳細エラーページ(500)
→ DEBUG=False: handler500 で定義されたページ(500)
│
▼
レスポンス後処理(通常通り内側 → 外側)
注意
process_exception はビュー関数の例外に対してのみ呼ばれます。
ミドルウェア自身の __call__ 内で発生した例外は、そのミドルウェアの外側のミドルウェアの __call__ に伝播するため、process_exception ではなく Python の通常の例外伝播メカニズムで処理されます。
これが「ミドルウェアの順序が重要」というもう一つの理由です。
最も外側のミドルウェアで例外が発生すると、他のミドルウェアのレスポンス後処理が一切実行されず、セッションの保存やヘッダーの付与が行われないまま生のエラーが返される可能性があります。
ミドルウェアチェーンの構造を理解すると、Vol.1「本書の対象読者とゴール」で整理した「ログはどこに出るか」「エラーはどの層で発生するか」という問いに対して、より正確な回答が可能になります。
次節では、ビュー関数が返した HttpResponse が WSGI サーバを経由してクライアントに届くまでの最後の工程を追跡します。
1.6. View の実行
前節でミドルウェアチェーンを通過したリクエストが、URL 解決で特定されたビュー関数に到達するまでを追いました。 本節では、Django がビュー関数を実際に呼び出す仕組みを、関数ベースビュー(FBV)とクラスベースビュー(CBV)の両方について内部から追跡します。
1.6.1. 関数ベースビュー(FBV)
関数ベースビューは Django の最もシンプルなビュー実装です。
request オブジェクトを第一引数として受け取り、HttpResponse を返す関数がそのままビューになります。
# users/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from .models import User
def user_list(request):
if request.method == "GET":
users = User.objects.all().values("id", "name", "email")
return JsonResponse(list(users), safe=False)
return JsonResponse({"error": "Method not allowed"}, status=405)
def user_detail(request, user_id):
user = get_object_or_404(User, id=user_id)
if request.method == "GET":
return JsonResponse({"id": user.id, "name": user.name, "email": user.email})
elif request.method == "DELETE":
user.delete()
return JsonResponse({"deleted": user_id}, status=204)
return JsonResponse({"error": "Method not allowed"}, status=405)
Django がこの関数を呼び出す箇所を追うと、前節で見たミドルウェアチェーンの最も内側にある BaseHandler._get_response にたどり着きます。
# django/core/handlers/base.py(概念的な構造)
class BaseHandler:
def _get_response(self, request):
resolver_match = resolve(request.path_info)
request.resolver_match = resolver_match
callback = resolver_match.func
callback_args = resolver_match.args
callback_kwargs = resolver_match.kwargs
# process_view フックの実行(前節参照)
for middleware_method in self._view_middleware:
response = middleware_method(request, callback, callback_args, callback_kwargs)
if response:
return response
# ビュー関数の呼び出し
response = callback(request, *callback_args, **callback_kwargs)
return response
callback は URL 解決で得られたビュー関数そのものです。
user_detail であれば callback(request, user_id=42) のように呼び出されます。
関数ベースビューの場合、Django が行う特別な処理はほとんどありません。
Python の通常の関数呼び出しとして実行され、戻り値の HttpResponse がそのままミドルウェアチェーンを逆順に返っていきます。
Tip
この直接性が関数ベースビューの強みであり、処理の流れを追跡しやすい理由でもあります。
リクエストが来て、関数が呼ばれて、レスポンスが返る。Vol.1「まずは 1 リクエストだけ処理するサーバを作る」の自作サーバで handle_request 関数を書いたときの感覚と本質的に同じです。
1.6.2. クラスベースビュー(CBV)
クラスベースビューは、ビューのロジックをクラスのメソッドとして構造化する仕組みです。
# users/views.py
from django.http import JsonResponse
from django.views import View
from django.shortcuts import get_object_or_404
from .models import User
class UserDetailView(View):
def get(self, request, user_id):
user = get_object_or_404(User, id=user_id)
return JsonResponse({"id": user.id, "name": user.name, "email": user.email})
def delete(self, request, user_id):
user = get_object_or_404(User, id=user_id)
user.delete()
return JsonResponse({"deleted": user_id}, status=204)
URL パターンには as_view() の戻り値を登録します。
# users/urls.py
from django.urls import path
from .views import UserDetailView
urlpatterns = [
path('<int:user_id>/', UserDetailView.as_view(), name='user-detail'),
]
ここで疑問が生じます。Django の URL 解決は callable を期待しているのに、クラスをどうやってビュー関数として扱うのでしょうか。
答えは as_view() にあります。
# django/views/generic/base.py(概念的な構造)
class View:
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
@classonlymethod
def as_view(cls, **initkwargs):
def view(request, *args, **kwargs):
self = cls(**initkwargs)
self.request = request
self.args = args
self.kwargs = kwargs
self.setup(request, *args, **kwargs)
return self.dispatch(request, *args, **kwargs)
return view
as_view() はクラスメソッドで、内部で view という関数を定義してそれを返します。
この view 関数こそが URL パターンに登録される callable です。
つまり Django の URL 解決から見れば、クラスベースビューであっても結局は「関数が呼ばれる」という構造に変わりありません。
重要
view 関数が呼ばれるたびに cls(**initkwargs) でクラスの 新しいインスタンス が生成されます。
リクエスト間でインスタンスが共有されないため、self に状態を保持しても他のリクエストに影響しません。
これは Gunicorn のマルチワーカー環境でも安全に動作するための設計です。
1.6.3. dispatch の流れ
as_view() が生成した view 関数の最後で self.dispatch() が呼ばれます。
dispatch はクラスベースビューの中核であり、HTTP メソッドに応じて適切なハンドラメソッドを呼び分けます。
# django/views/generic/base.py(概念的な構造)
class View:
def dispatch(self, request, *args, **kwargs):
method = request.method.lower()
if method in self.http_method_names:
handler = getattr(self, method, self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
def http_method_not_allowed(self, request, *args, **kwargs):
return HttpResponseNotAllowed(self._allowed_methods())
dispatch の処理を順に追います。
request.methodを小文字に変換します(例:"GET"→"get")。http_method_namesリストに含まれるかを確認します。含まれていれば
getattr(self, method)でインスタンスのメソッドを取得します。UserDetailViewにgetメソッドが定義されていればself.getが、deleteメソッドがあればself.deleteが取得されます。メソッドが定義されていない場合(たとえば POST リクエストが来たが
postメソッドがない場合)、getattrの第三引数によりself.http_method_not_allowedがフォールバックとして使われ、405 レスポンスが返されます。
この仕組みにより、関数ベースビューで手動で書いていた if request.method == "GET": ... elif request.method == "DELETE": ... という分岐が、メソッド名の規約によって自動化されます。
dispatch を時系列で図示します。
as_view() が返した view 関数が呼ばれる
│
├─ cls() でインスタンス生成
├─ self.request = request
├─ self.args = args
├─ self.kwargs = kwargs
│
▼
self.dispatch(request, *args, **kwargs)
│
├─ method = request.method.lower() → "get"
├─ handler = getattr(self, "get", self.http_method_not_allowed)
│ → self.get が存在すれば self.get
│ → 存在しなければ self.http_method_not_allowed
│
▼
handler(request, *args, **kwargs)
│
└─ self.get(request, user_id=42) → JsonResponse({...})
Django が提供する汎用ビュー(ListView, DetailView, CreateView など)はすべてこの View.dispatch を継承しています。
たとえば DetailView は get メソッドの中でオブジェクトの取得とテンプレートレンダリングを行い、CreateView は get でフォーム表示、post でフォーム処理を行います。
いずれも dispatch による HTTP メソッドの振り分けという基盤の上に成り立っています。
1.6.4. request / args / kwargs
ビュー関数に渡される引数を改めて整理します。 関数ベースビューでもクラスベースビューでも、渡される情報は同じ三種類です。
request: 1.3 章(リクエストは Django にどう渡るか)で詳しく見たWSGIRequestのインスタンスです。request.method,request.path,request.GET,request.body,request.headers,request.user(ミドルウェアが設定)などの属性を持ちます。args(位置引数):re_pathで名前なしグループ([0-9]+)を使った場合に文字列として渡されます。path()を使っている場合、位置引数は通常空タプルです。kwargs(キーワード引数):path('<int:user_id>/', ...)で定義したパスパラメータがuser_id=42のように渡されます。パスコンバータによる型変換は URL 解決の段階で完了しているため、ビュー関数が受け取る時点でint型になっています。
# 関数ベースビュー: 直接引数として受け取る
def user_detail(request, user_id):
# request: WSGIRequest
# user_id: 42 (int)
...
# クラスベースビュー: 引数としても self 経由でも取得可能
class UserDetailView(View):
def get(self, request, user_id):
# 引数から直接
print(user_id) # 42
# self 経由(dispatch 前に設定済み)
print(self.kwargs) # {"user_id": 42}
print(self.request.method) # "GET"
...
クラスベースビューでは dispatch が呼ばれる前に self.request, self.args, self.kwargs が設定されるため、ハンドラメソッド内でどちらの方法でもアクセスできます。
Mixin を使って複数のビューで共通処理を記述する場合、self.kwargs 経由のアクセスが便利です。
ビュー関数が HttpResponse を返すと、その戻り値は _get_response に戻り、ミドルウェアチェーンのレスポンス後処理を逆順に通過して、最終的に WSGI サーバへ渡されます。
この一連の流れは、Vol.1「WSGI が生まれた背景」の WSGI callable が start_response を呼んでイテラブルを返す構造と、抽象度は異なりますが本質的に同じです。
次節では、ビュー関数が返した HttpResponse が WSGI インタフェースを通じてサーバへ渡され、最終的にクライアントに届くまでのレスポンス返却処理を追跡します。
1.7. レスポンス生成
前節でビュー関数が呼び出されるまでの経路を追いました。
本節では、ビュー関数が返す HttpResponse オブジェクトの内部構造を解剖し、Django がどのようにしてレスポンスを組み立て、最終的に WSGI インタフェースへ渡すかを追跡します。
Vol.1「まずは 1 リクエストだけ処理するサーバを作る」で自作した make_response() 関数がステータスライン・ヘッダー・ボディを手動で組み立てていた処理を、Django がどのように抽象化しているかを見ていきましょう。
1.7.1. HttpResponse
HttpResponse は Django のレスポンス体系の基底クラスです。
すべてのレスポンスクラスはこのクラスを継承しています。
from django.http import HttpResponse
# 最もシンプルな使い方
response = HttpResponse("Hello, Django!")
response = HttpResponse("<h1>Welcome</h1>", content_type="text/html; charset=utf-8")
response = HttpResponse(status=204)
内部構造をソースコード(django/http/response.py)に沿って追うと、HttpResponse は驚くほど素朴な仕組みで動いています。
# django/http/response.py(概念的な構造)
class HttpResponseBase:
status_code = 200
def __init__(self, content_type=None, status=None, reason=None, charset=None):
self.headers = ResponseHeaders()
if content_type is None:
content_type = f"text/html; charset={self.charset}"
self.headers["Content-Type"] = content_type
if status is not None:
self.status_code = status
if reason is not None:
self.reason_phrase = reason
else:
self.reason_phrase = responses.get(self.status_code, "Unknown Status Code")
def __setitem__(self, header, value):
self.headers[header] = value
def __getitem__(self, header):
return self.headers[header]
class HttpResponse(HttpResponseBase):
def __init__(self, content=b"", content_type=None, status=None, reason=None):
super().__init__(content_type=content_type, status=status, reason=reason)
self.content = content
@property
def content(self):
return b"".join(self._container)
@content.setter
def content(self, value):
if isinstance(value, str):
value = value.encode(self.charset)
self._container = [value]
この構造から読み取れるポイントは次の三つです。
自動エンコード:
contentプロパティのセッターが文字列を自動的にバイト列へエンコードします。HttpResponse("こんにちは")と書くと、内部で"こんにちは".encode("utf-8")が実行されます。Vol.1でバイト列と文字列の変換を手動で行っていた処理が、ここで自動化されています。イテラブルとしての
_container: WSGI の仕様がレスポンスボディをイテラブルとして要求しているため、_containerはリストとして保持されます。HttpResponseは__iter__メソッドを持ち、_containerをイテレートすることで WSGI サーバにバイト列を渡します。辞書的なヘッダー操作: ヘッダーの操作が
response["X-Custom-Header"] = "value"のように辞書的なインタフェースで行えます。Vol.1「WSGI の上に何が必要になるのか」で自作したミドルウェアがstart_responseのヘッダーリストを直接操作していたのに比べて大幅に扱いやすくなっています。
HttpResponse が WSGI サーバに渡される最終段階は、WSGIHandler.__call__ の中で行われます。
# django/core/handlers/wsgi.py(概念的な構造)
class WSGIHandler:
def __call__(self, environ, start_response):
request = WSGIRequest(environ)
response = self.get_response(request)
status = f"{response.status_code} {response.reason_phrase}"
headers = list(response.items()) # [("Content-Type", "..."), ...]
start_response(status, headers)
return response # HttpResponse は __iter__ を持つイテラブル
Vol.1「WSGI が生まれた背景」で学んだ start_response(status, headers) と「バイト列のイテラブルを返す」という WSGI の約束事が、HttpResponse のインタフェース設計にそのまま反映されていることが分かります。
Django は HttpResponse のサブクラスとして、ステータスコード固定のショートカットクラスも提供しています。
クラス |
ステータスコード |
|---|---|
|
404 |
|
403 |
|
302 |
|
301 |
|
405 |
|
500 |
いずれも HttpResponse を継承し、status_code クラス変数を上書きしているだけです。
1.7.2. JsonResponse
REST API を構築する際に頻繁に使う JsonResponse は、HttpResponse を継承した薄いラッパーです。
from django.http import JsonResponse
# 辞書を渡す
response = JsonResponse({"id": 42, "name": "Taro", "email": "[email protected]"})
# リストを渡す場合は safe=False が必要
response = JsonResponse([{"id": 1}, {"id": 2}], safe=False)
# ステータスコードの指定
response = JsonResponse({"error": "Not Found"}, status=404)
内部実装は以下の通りです。
# django/http/response.py(概念的な構造)
class JsonResponse(HttpResponse):
def __init__(self, data, encoder=DjangoJSONEncoder, safe=True,
json_dumps_params=None, **kwargs):
if safe and not isinstance(data, dict):
raise TypeError(
"In order to allow non-dict objects to be serialized, "
"set the safe parameter to False."
)
if json_dumps_params is None:
json_dumps_params = {}
kwargs.setdefault("content_type", "application/json")
data = json.dumps(data, cls=encoder, **json_dumps_params)
super().__init__(content=data, **kwargs)
JsonResponse が行っていることは次の三つです。
safe=Trueのとき辞書以外のデータを拒否する型チェックjson.dumpsによる JSON シリアライズContent-Typeをapplication/jsonに設定する処理
Vol.1「WSGI が生まれた背景」で自作した json_response ヘルパー関数と本質的に同じことを、Django のレスポンス体系に統合した形で実現しています。
safe パラメータが存在する理由は、JSON のトップレベルが配列であるレスポンスに対する歴史的なセキュリティ上の懸念(JSON Hijacking)に起因します。
現代のブラウザではこの問題は解消されていますが、Django は明示的な safe=False の指定を求めることで、開発者に意図を確認させています。
注釈
DjangoJSONEncoder は Python 標準の json.JSONEncoder を拡張し、datetime, date, time, Decimal, UUID などの型を自動的にシリアライズできるようにしています。
ORM から取得したモデルインスタンスを直接渡すことはできないため、values() や手動の辞書構築、あるいは Django REST Framework のシリアライザを使う必要があります。
1.7.3. TemplateResponse
TemplateResponse は、テンプレートのレンダリングを遅延実行するレスポンスクラスです。
from django.template.response import TemplateResponse
def user_profile(request, user_id):
user = get_object_or_404(User, id=user_id)
context = {"user": user, "posts": user.posts.all()}
return TemplateResponse(request, "users/profile.html", context)
通常の render ショートカットとの違いは、レスポンスが返される時点ではまだテンプレートのレンダリングが実行されていないという点です。
# django/template/response.py(概念的な構造)
class TemplateResponse(SimpleTemplateResponse):
def __init__(self, request, template, context=None, content_type=None,
status=None, using=None):
super().__init__(template, context, content_type, status, using)
self._request = request
@property
def content(self):
if not self._is_rendered:
self.render()
return super().content
def render(self):
template = self.resolve_template(self.template_name)
context = self.resolve_context(self.context_data)
self.content = template.render(context, self._request)
return self
Tip
この遅延レンダリングの設計は、前節で見た process_template_response ミドルウェアフックと連動しています。
ビュー関数が TemplateResponse を返した後、ミドルウェアは response.context_data を変更したり、response.template_name を差し替えたりできます。
レンダリングは全てのミドルウェアの process_template_response が完了した後に初めて実行されます。
ビュー関数が TemplateResponse を返す(未レンダリング)
│
▼
process_template_response(内側 → 外側)
├─ ミドルウェア A: response.context_data["extra"] = "value" を追加
├─ ミドルウェア B: response.template_name を差し替え
│
▼
response.render() ← ここで初めてテンプレートが処理される
│
▼
レスポンス後処理(通常の __call__ フロー)
一方、render(request, template_name, context) ショートカットは即座にテンプレートをレンダリングし、通常の HttpResponse を返します。
ミドルウェアでテンプレートやコンテキストを操作する必要がない場合は render の方がシンプルです。
1.7.4. ストリーミングレスポンス
大容量のデータをレスポンスとして返す場合、全体をメモリ上に構築してから送信するのは非効率です。
StreamingHttpResponse はジェネレータを受け取り、チャンク単位でクライアントに送信します。
from django.http import StreamingHttpResponse
import csv
def export_users_csv(request):
users = User.objects.all().iterator()
def generate():
yield "id,name,email\n"
for user in users:
yield f"{user.id},{user.name},{user.email}\n"
response = StreamingHttpResponse(generate(), content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="users.csv"'
return response
StreamingHttpResponse の内部構造は HttpResponse とは本質的に異なります。
# django/http/response.py(概念的な構造)
class StreamingHttpResponse(HttpResponseBase):
def __init__(self, streaming_content=(), content_type=None, status=None, reason=None):
super().__init__(content_type=content_type, status=status, reason=reason)
self.streaming_content = streaming_content
@property
def streaming_content(self):
return map(self.make_bytes, self._iterator)
@streaming_content.setter
def streaming_content(self, value):
self._iterator = iter(value)
def __iter__(self):
return self.streaming_content
HttpResponse がボディ全体を _container リストに保持するのに対し、StreamingHttpResponse はイテレータへの参照だけを保持します。
WSGI サーバが __iter__ でチャンクを取得するたびにジェネレータの __next__ が呼ばれ、次のチャンクだけが生成されます。
Vol.1「WSGI が生まれた背景」で学んだ「ジェネレータを返す WSGI アプリ」の仕組みがそのまま活かされています。
注意
StreamingHttpResponse にはいくつかの制約があります。
ボディ全体が確定していないため
Content-Lengthヘッダーが設定できず、Transfer-Encoding: chunkedに依存します。contentプロパティへのアクセスはできません(イテレータは一度しかイテレートできないため)。ミドルウェアの中には
response.contentを参照するものがあり、そうしたミドルウェアとの併用には注意が必要です。
FileResponse は StreamingHttpResponse のサブクラスで、ファイルオブジェクトの送信に特化しています。
from django.http import FileResponse
def download_report(request, report_id):
report = get_object_or_404(Report, id=report_id)
return FileResponse(open(report.file.path, "rb"), as_attachment=True,
filename=f"report_{report_id}.pdf")
FileResponse は os.fstat でファイルサイズを取得して Content-Length を設定し、wsgi.file_wrapper(environ に含まれる場合)を利用してカーネルレベルの sendfile システムコールによる高速転送を試みます。
これは WSGI 仕様で定義されたオプション機能で、ユーザースペースを経由せずにファイルデータをソケットに直接送信できるため、大容量ファイルのダウンロードで大きなパフォーマンス上の利点があります。
本節で Django のレスポンス体系を概観しました。 各クラスと Vol.1 の自作コードの対応関係は次のとおりです。
Django クラス |
Vol.1 相当 |
|---|---|
|
|
|
|
|
WSGI のジェネレータ返却 |
抽象化の層は厚くなっていますが、WSGI の「ステータスとヘッダーを start_response で送り、ボディをイテラブルで返す」という基本構造は変わっていません。
次節では、Django の WSGI 処理全体を振り返り、トラブルシューティングの観点から各工程を総括します。
1.8. Django が面倒を見てくれているもの
ここまでの節で、リクエストが WSGIHandler に届き、WSGIRequest に変換され、ミドルウェアチェーンを通過し、URL 解決を経てビュー関数が呼ばれ、HttpResponse が返されるまでの一連の流れを追いました。
この過程で何度も「ミドルウェアが処理する」「Django が自動的に変換する」と記述してきましたが、本節ではそれらの自動処理を横断的に整理します。
普段ビュー関数を書くだけでは意識しない、Django が裏側で引き受けている責務の全体像を把握することが目的です。
1.8.1. CSRF
Cross-Site Request Forgery(CSRF)は、ユーザーが意図しないリクエストを、認証済みのセッションを悪用して送信させる攻撃です。
Django の CsrfViewMiddleware はこの攻撃に対する防御をほぼ完全に自動化しています。
仕組みを内側から見ると、処理は次の二つの段階に分かれます。
GET リクエスト時: テンプレート内の
{% csrf_token %}タグが hidden フィールドとしてトークンを埋め込み、同時にレスポンスのSet-Cookieヘッダーで同じトークンを Cookie にも保存します。POST リクエスト時:
CsrfViewMiddlewareのprocess_viewフックがフォームから送信されたトークンと Cookie のトークンを比較し、一致しなければ 403 を返します。
# テンプレート側
<form method="POST" action="/users/">
{% csrf_token %}
<input name="name" value="Taro">
<button type="submit">Create</button>
</form>
<!-- レンダリング結果 -->
<form method="POST" action="/users/">
<input type="hidden" name="csrfmiddlewaretoken"
value="a1b2c3d4e5f6...">
<input name="name" value="Taro">
<button type="submit">Create</button>
</form>
REST API でセッション認証を使わず、トークン認証や JWT で保護する場合、CSRF 保護は不要です。
その場合はビュー関数に @csrf_exempt デコレータを付与するか、Django REST Framework がデフォルトで SessionAuthentication 以外の認証クラスに対して CSRF チェックを無効化する仕組みを利用します。
注釈
CsrfViewMiddleware が process_view フックを使っているのには設計上の理由があります。
process_view はビュー関数が特定された後に呼ばれるため、ビューに @csrf_exempt が付いているかどうかを確認できます。
リクエスト前処理の段階(__call__ の前半)ではビュー関数が未確定であるため、この判断ができません。
ミドルウェアフックの使い分けが、具体的な機能実装に直結している好例です。
1.8.2. セッション
HTTP はステートレスなプロトコルであり、連続するリクエスト間でユーザーの状態を保持する仕組みは HTTP 自体には存在しません。
Django の SessionMiddleware はこの問題を、Cookie とサーバサイドのストレージを組み合わせて解決しています。
リクエスト前処理の段階で、SessionMiddleware は Cookie ヘッダーからセッション ID(デフォルトでは sessionid)を取得し、そのIDに紐づくセッションデータをストレージ(データベース、キャッシュ、ファイルなど)から読み込みます。読み込まれたデータは request.session として辞書ライクなオブジェクトで提供されます。
def add_to_cart(request):
cart = request.session.get("cart", [])
cart.append({"item_id": 42, "quantity": 1})
request.session["cart"] = cart
return JsonResponse({"cart_size": len(cart)})
レスポンス後処理の段階では、セッションデータが変更されていた場合にのみストレージへ書き戻し、Set-Cookie ヘッダーを設定します。
セッションが変更されていなければ書き込みは発生しません。
この遅延保存の仕組みにより、セッションを使わないリクエスト(API の GET リクエストなど)ではストレージへの書き込みコストが発生しません。
Tip
SessionMiddleware が MIDDLEWARE リストの上位に配置される理由は、後続のミドルウェア(特に AuthenticationMiddleware)が request.session に依存しているためです。
セッションが利用可能になっていなければ、ユーザーの認証状態を復元できません。
1.8.3. 認証
AuthenticationMiddleware は SessionMiddleware が設定した request.session からユーザー ID を取得し、データベースからユーザーオブジェクトを復元して request.user に設定します。
# django/contrib/auth/middleware.py(概念的な構造)
class AuthenticationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not hasattr(request, "session"):
raise ImproperlyConfigured(
"The Django authentication middleware requires session middleware "
"to be installed."
)
request.user = SimpleLazyObject(lambda: get_user(request))
response = self.get_response(request)
return response
SimpleLazyObject は Django のユーティリティで、request.user が初めてアクセスされた時点でデータベースクエリを実行します。
ビュー関数が request.user を参照しなければクエリは発生しません。
これは1.3 章(リクエストは Django にどう渡るか)で見た request.body の遅延読み取りと同じ設計思想です。
ユーザーがログインしていない場合、request.user は AnonymousUser インスタンスになります。
AnonymousUser は is_authenticated が False を返し、id が None であるオブジェクトで、ビュー関数側で if request.user.is_authenticated: と分岐するだけで認証の有無を判定できます。
ログイン・ログアウトの処理は次の関数が担当します。
ログイン:
django.contrib.auth.login(request, user)— セッションにユーザー ID を書き込みます。ログアウト:
django.contrib.auth.logout(request)— セッションをフラッシュします。
いずれもセッション機構の上に構築されているため、SessionMiddleware → AuthenticationMiddleware の順序依存は必然的なものです。
1.8.4. 例外ページ
Django はビュー関数やミドルウェアで発生した例外を自動的に HTTP レスポンスに変換します。
この仕組みは BaseHandler._get_response の内部と、前節で解説した process_exception フックの組み合わせで実現されています。
例外の種類と対応するレスポンスの関係を整理すると次のとおりです。
例外クラス |
HTTP ステータス |
|---|---|
|
404 Not Found |
|
403 Forbidden |
|
400 Bad Request |
その他の未処理例外 |
500 Internal Server Error |
DEBUG 設定により例外ページの内容が劇的に変わります。
DEBUG=True: Django は詳細なスタックトレース、ローカル変数の値、request.METAの内容、SQL クエリの履歴を含む HTML ページを生成します。開発中はこのページが強力なデバッグツールになります。DEBUG=False:handler404,handler500などで定義されたカスタムテンプレート(または Django デフォルトの簡素なページ)が表示されます。
# urls.py でカスタムエラーハンドラを設定
handler404 = 'myproject.views.custom_404'
handler500 = 'myproject.views.custom_500'
危険
本番環境で DEBUG=True を有効にしてはいけません。
例外ページにはデータベースの接続情報、シークレットキー、環境変数、ファイルパスなど、攻撃者にとって有用な情報が大量に含まれます。
Vol.1「WSGI の上に何が必要になるのか」で触れた Werkzeug のインタラクティブデバッガと同様、開発用の便利機能は本番では致命的なセキュリティホールになります。
1.8.5. 設定管理
Django の settings.py はフレームワーク全体の挙動を制御する単一の設定ファイルです。
ミドルウェアの構成、データベース接続、テンプレートエンジン、静的ファイルの配信、セキュリティ関連のフラグなど、数百の設定項目が一箇所に集約されています。
1 章(Django を WSGI 視点で見る)で見た通り、wsgi.py の os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') により、どの設定ファイルを読み込むかが決定されます。
django.setup() の実行時に設定が読み込まれ、django.conf.settings というグローバルオブジェクトを通じてプロジェクト全体からアクセス可能になります。
from django.conf import settings
if settings.DEBUG:
print("Debug mode is enabled")
print(settings.DATABASES["default"]["ENGINE"])
settings オブジェクトは遅延ロードの仕組みを持っており、最初にアクセスされた時点で設定モジュールをインポートします。
設定項目にはすべてデフォルト値が定義されており(django/conf/global_settings.py)、プロジェクトの settings.py で明示的に指定しなかった項目はデフォルト値が使われます。
環境ごとの設定切り替え(開発・ステージング・本番)は DJANGO_SETTINGS_MODULE 環境変数を変えることで実現します。
myproject.settings.dev, myproject.settings.production のように設定ファイルを分割し、共通部分はベースファイルからインポートするパターンが一般的です。
この仕組みがあるため、同じコードベースを異なる環境にデプロイする際にアプリケーションコードを変更する必要がありません。
セキュリティ上特に重要な設定項目とその影響は次のとおりです。
設定項目 |
用途 |
誤設定時のリスク |
|---|---|---|
|
セッション署名・CSRF トークン生成 |
漏洩するとセッションの偽造が可能 |
|
許可するホスト名のリスト |
|
|
デバッグモードの有効化 |
|
Django は DEBUG=False かつ ALLOWED_HOSTS が未設定の場合に起動時エラーを出すなど、危険な設定を検出する仕組みも備えています。
本節で整理した CSRF 防御、セッション管理、認証、例外ハンドリング、設定管理は、いずれもビュー関数のコードには直接現れません。
開発者が request.user.is_authenticated と書くだけでユーザー認証が判定でき、POST フォームに {% csrf_token %} を入れるだけで CSRF 攻撃を防げるのは、これらの仕組みがミドルウェアと設定システムに組み込まれているからです。
しかしトラブルが発生したとき、たとえば「ログインしているはずなのに request.user が AnonymousUser になる」「CSRF 検証が失敗する」「本番で詳細なエラーページが表示される」といった問題に直面したとき、これらの内部構造を知っていることが解決への最短経路になります。
次節では1 章(Django を WSGI 視点で見る)全体を振り返り、トラブルシューティングの観点で各工程を総括します。
1.9. トラブルシューティングの観点
1 章(Django を WSGI 視点で見る)を通じて Django の WSGI ハンドラからビュー関数、レスポンス生成までの内部構造を追跡してきました。 本節ではその知識を実際の問題解決に応用します。 ここで取り上げる5つのケースはいずれも「Django のコードは正しく書いたはずなのに動かない」という状況で頻出するものであり、内部構造を知らなければ原因の特定に時間を要する問題ばかりです。
1.9.1. URL 解決ミス
「ビュー関数を書いたのに 404 が返る」は Django 開発で最も多いトラブルの一つです。 原因は複数の層にまたがるため、切り分けが重要になります。
確認すべき点を順に挙げます。
1. URL パターンの登録漏れ
ビュー関数を書いても urls.py の urlpatterns に追加していなければ、Django の URL 解決木にそのパスは存在しません。
DEBUG=True の環境で 404 ページを表示すると、Django は登録済みの全 URL パターンを一覧表示してくれるため、自分のパスがリストに含まれているかをまず確認します。
2. include のパス結合
ルートの urls.py で path('api/users/', include('users.urls')) と書き、users/urls.py で path('/detail/', ...) とスラッシュから始めてしまうと、結合結果が意図しないパスになります。
include で結合されるパスは先頭にスラッシュを付けないのが原則です。
# NG: 先頭スラッシュ
path('/detail/', views.user_detail) # → /api/users//detail/ になる可能性
# OK: 先頭スラッシュなし
path('detail/', views.user_detail) # → /api/users/detail/
3. パスコンバータの型不一致
path('<int:user_id>/', ...) に対して /users/abc/ がリクエストされると、int への変換が失敗し、そのパターンはマッチしません。
全パターンがマッチしなければ 404 になります。
エラーメッセージは「型変換に失敗した」ではなく単に「ページが見つからない」なので、パスコンバータの型を見直す必要があります。
4. 末尾スラッシュの不一致
Django はデフォルトで APPEND_SLASH=True が設定されており、CommonMiddleware が末尾スラッシュのないリクエストを自動的にリダイレクトします。
ただしこのリダイレクトは urlpatterns にスラッシュ付きのパターンが存在する場合にのみ発生します。
注意
POST リクエストに対して 301 リダイレクトが発生すると、リダイレクト先が GET になるためデータが消失する可能性があります。
API では末尾スラッシュの有無を統一するか、APPEND_SLASH=False に設定して明示的に管理するのが安全です。
原因の切り分けには django.urls.resolve を対話型シェルで使うのが効果的です。
python manage.py shell
>>> from django.urls import resolve, Resolver404
>>> try:
... match = resolve('/api/users/42/')
... print(match.func, match.kwargs)
... except Resolver404:
... print("No URL pattern matched.")
1.9.2. ミドルウェアの順序問題
1.5 章(middleware chain の流れ)で解説した通り、MIDDLEWARE リストの順序は依存関係を反映しています。
順序を誤ると、一見すると無関係に見えるエラーが発生します。
典型的なのは AuthenticationMiddleware を SessionMiddleware より前に配置したケースです。
AuthenticationMiddleware は request.session からユーザー ID を取得しますが、SessionMiddleware がまだ実行されていなければ request.session が存在せず、AttributeError や ImproperlyConfigured が発生します。
# NG: 認証がセッションより前
MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', # request.session がない
'django.contrib.sessions.middleware.SessionMiddleware',
...
]
# OK: セッション → 認証の順
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
]
もう一つの頻出パターンは、カスタムミドルウェアの配置場所です。
request.userを記録するロギングミドルウェア:AuthenticationMiddlewareより後に配置する必要があります。全リクエストに対して実行したいセキュリティチェック:
SecurityMiddlewareの直後、他のミドルウェアより前に配置するのが適切です。
順序問題のデバッグには、各ミドルウェアの __call__ の先頭でログを出力する方法が有効です。
import logging
logger = logging.getLogger(__name__)
class DebugMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
logger.debug(f"[DebugMiddleware] Before: "
f"has session={hasattr(request, 'session')}, "
f"has user={hasattr(request, 'user')}")
response = self.get_response(request)
logger.debug(f"[DebugMiddleware] After: status={response.status_code}")
return response
このミドルウェアを MIDDLEWARE リストの異なる位置に挿入することで、各段階で request にどの属性が設定されているかを確認できます。
1.9.3. request body の扱い
Django の request.body に関するトラブルは、1.3 章(リクエストは Django にどう渡るか)で解説した遅延読み取りとストリームの性質に起因するものが大半です。
最も多いのは、ミドルウェアで request.body を読み取った後、ビュー関数でも読み取ろうとするケースです。
Django の HttpRequest は body プロパティにキャッシュ機構を持っているため、通常は二回目のアクセスもキャッシュから返されます。
しかしカスタムミドルウェアが request.read() や request._stream.read() を直接呼んだ場合、キャッシュが効かずにストリームが消費され、ビュー関数側で空のボディが返されることがあります。
# NG: ストリームを直接消費
class BadMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
body = request._stream.read() # ストリームを消費
print(f"Body: {body}")
return self.get_response(request)
# → ビュー関数で request.body が空になる
# OK: request.body を使う(キャッシュされる)
class GoodMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
body = request.body # キャッシュされるため安全
print(f"Body: {body}")
return self.get_response(request)
JSON ボディの扱いにも注意が必要です。
Django は request.POST に JSON データを格納しません。
Content-Type: application/json で送信されたデータは json.loads(request.body) で明示的にパースする必要があります。
警告
フォーム送信に慣れた開発者が request.POST を参照して空の QueryDict を受け取り困惑するのはよくある光景です。
JSON ボディは request.POST ではなく request.body から取得してください。
# フォームデータ(application/x-www-form-urlencoded)
name = request.POST.get("name") # OK
# JSON データ(application/json)
name = request.POST.get("name") # NG: 常に None
import json
data = json.loads(request.body) # OK
name = data.get("name")
Content-Length と実際のボディサイズの不一致も問題を引き起こします。
リバースプロキシがリクエストを加工した場合や、クライアント実装のバグで Content-Length が正しく設定されていない場合、request.body の読み取りがタイムアウトしたり、データが切り詰められたりします。
Vol.1「HTTP は何をやりとりしているのか」で解説した Content-Length の役割と、Vol.1「WSGI が生まれた背景」で見た wsgi.input の読み取り方を理解していれば、この問題の原因を推測できます。
1.9.4. ALLOWED_HOSTS
本番環境で最も頻出する Django 固有のエラーが DisallowedHost です。
DEBUG=False の環境で ALLOWED_HOSTS にリクエストのホスト名が含まれていないと、Django は 400 Bad Request を返します。
# settings.py
DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]
この設定で curl -H "Host: evil.com" http://your-server/ がリクエストされると、Django は HTTP Host ヘッダー攻撃を防ぐために 400 を返します。
Vol.1「WSGI が生まれた背景」で学んだ environ["HTTP_HOST"] の値がここで検証されています。
問題が起きやすいのは、ロードバランサーや CDN を経由した場合です。
クライアントは https://example.com にアクセスしていても、ロードバランサーが Django サーバに転送する際のホスト名が内部 IP アドレスやロードバランサーのホスト名になっていることがあります。
この場合は次のいずれかの対応が必要です。
ALLOWED_HOSTSにそのホスト名を追加するロードバランサーの設定で元のホスト名を
Hostヘッダーに保持するようにする
デバッグの際は、まず Django のログに出力される DisallowedHost の詳細メッセージを確認します。
どのホスト名が拒否されたかが記録されているため、ALLOWED_HOSTS に何を追加すべきかが分かります。
# settings.py(ロギング設定)
LOGGING = {
'version': 1,
'handlers': {
'console': {'class': 'logging.StreamHandler'},
},
'loggers': {
'django.security.DisallowedHost': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}
警告
開発中に ALLOWED_HOSTS = ["*"] と設定するのは便利ですが、この設定を本番にデプロイすると Host ヘッダー攻撃に脆弱になります。
Django の check --deploy コマンドはこうした危険な設定を検出してくれます。
1.9.5. 静的ファイル配信の誤解
「開発サーバでは CSS や JavaScript が表示されるのに、本番にデプロイしたら表示されなくなった」は Django 初学者が必ず遭遇する問題です。 これは Django の静的ファイル配信の設計思想を理解していないことに起因します。
開発環境(
DEBUG=True):python manage.py runserverの開発サーバはdjango.contrib.staticfilesが自動的に静的ファイルを配信します。各アプリのstatic/ディレクトリからファイルを探し、/static/css/style.cssのようなリクエストに対して直接レスポンスを返します。本番環境(
DEBUG=False): Django は静的ファイルを一切配信しません。
重要
これは意図的な設計です。Django は Python のアプリケーションサーバであり、静的ファイルの配信は Nginx や CDN が担うべき責務です。 Vol.1「本書の対象読者とゴール」で整理した「役者」の分担で言えば、静的ファイルはリバースプロキシ層の仕事です。
本番環境での正しい手順は次のとおりです。
python manage.py collectstaticを実行して全アプリの静的ファイルをSTATIC_ROOTディレクトリに集約します。Nginx がそのディレクトリを直接配信するように設定します。
# nginx.conf
server {
location /static/ {
alias /var/www/myproject/staticfiles/;
}
location / {
proxy_pass http://127.0.0.1:8000;
}
}
この設定により、/static/ で始まるリクエストは Nginx が直接ファイルを返し、それ以外のリクエストだけが Django(Gunicorn)に転送されます。
Nginx はファイル配信に最適化されており、Django を経由するよりも桁違いに高速です。
whitenoise というライブラリを使えば Django 自身が静的ファイルを配信することも可能ですが、これは Heroku のように Nginx を前段に置けない環境向けの回避策であり、Nginx が利用可能な環境では Nginx に配信を任せるのが標準的なアプローチです。
本節で取り上げた5つのケースを振り返ります。
トラブル |
根本原因 |
|---|---|
URL 解決ミス |
フレームワーク内部の木構造探索 |
ミドルウェア順序問題 |
チェーン構築時の依存関係 |
request body の扱い |
WSGI ストリームの性質 |
|
HTTP Host ヘッダーの検証 |
静的ファイル配信の誤解 |
サーバ層とアプリ層の責務分担 |
Vol.1「HTTP は何をやりとりしているのか」から Vol.1「WSGI の上に何が必要になるのか」で積み上げた低レイヤの知識と、本章で追跡した Django の内部構造が、これらの問題を「見える」ものに変えてくれます。
次節では、本章の締めくくりとして Django とその外側にあるサーバ群の責務境界を明確にします。「この問題は Django を直すべきか、それともサーバ設定の問題か」という判断を正確に下せるようになることが目的です。
1.10. どこまでが Django の責務で、どこからがサーバの責務か
1 章(Django を WSGI 視点で見る)の締めくくりとして、Django とその外側にあるサーバ群の責務境界を明確にします。 Vol.1「本書の対象読者とゴール」で「役者を整理する」として6つのコンポーネントを概観しましたが、本節ではそれを Django の実装に即して具体化します。 「この問題は Django のコードを直すべきか、サーバの設定を変えるべきか、Nginx の設定を見るべきか」という判断を正確に下せるようになることが目的です。
1.10.1. 開発サーバ
python manage.py runserver で起動する開発サーバは、Django に同梱された簡易的な WSGI サーバです。
内部的には Python 標準ライブラリの wsgiref.simple_server をベースに、静的ファイル配信とオートリロードの機能を追加したものです。
開発サーバは一つのプロセス内でリクエストを逐次処理します。 Django 4.0 以降はデフォルトでスレッドを使った並行処理が有効になっていますが、それでも本番のワークロードには対応できません。 このサーバが担っているのは次の処理です。
TCP ソケットのリッスン
HTTP リクエストのパース
environ辞書の構築WSGIHandler.__call__の呼び出しレスポンスのソケットへの書き出し
つまり Vol.1「まずは 1 リクエストだけ処理するサーバを作る」で自作した HTTP サーバと Vol.1「WSGI が生まれた背景」の wsgiref が行っていた処理を、Django の開発用に統合したものに過ぎません。
開発サーバが本番に適さない理由を整理すると次のとおりです。
制約 |
詳細 |
|---|---|
ワーカー管理なし |
一つのリクエストがクラッシュするとサーバ全体が停止する可能性があります |
タイムアウト管理が簡素 |
Slowloris 攻撃のような悪意あるリクエストに脆弱です |
TLS 非対応 |
HTTPS を直接提供できません |
静的ファイル最適化なし |
|
Tip
開発サーバの存在意義は、Gunicorn や Nginx の設定なしにコードの動作確認ができるという一点に集約されます。
runserver で動くアプリケーションは、そのまま Gunicorn でも動きます。
なぜなら両者は同じ WSGI インタフェースで Django の application callable を呼び出しているからです。
1.10.2. WSGI サーバ
本番環境では Gunicorn や uWSGI といった WSGI サーバが開発サーバの役割を引き継ぎます。
WSGI サーバの責務は、Django の application(environ, start_response) を呼び出す までの すべての処理と、HttpResponse イテラブルを受け取って からの すべての処理です。
具体的には、次の項目が WSGI サーバの責務です。
TCP ソケットの管理(バインド、リッスン、accept)
HTTP リクエストのパース(Vol.1「HTTP は何をやりとりしているのか」の内容)
environ辞書の構築(Vol.1「WSGI が生まれた背景」の内容)ワーカープロセスの生成と管理
リクエストのワーカーへの振り分け
ワーカーのクラッシュ検知と再起動
タイムアウトの監視
レスポンスバイト列のソケットへの書き出し
Gunicorn を例にとると、マスタープロセスが起動時に wsgi.py から application を読み込み、--workers で指定された数のワーカープロセスを fork します。
各ワーカーは独立したプロセスとして TCP ソケットを共有し、リクエストを受け取るたびに application(environ, start_response) を呼び出します。
gunicorn myproject.wsgi:application \
--workers 4 \
--timeout 30 \
--bind 127.0.0.1:8000
この構成において、Django が責任を負う範囲と Gunicorn が責任を負う範囲は WSGI インタフェースで明確に分離されています。
Gunicorn の責務 Django の責務
───────────────── ─────────────────
TCP ソケット管理 WSGIRequest の生成
HTTP パース → environ 構築 ミドルウェアチェーン
ワーカープロセス管理 URL 解決
タイムアウト監視 ビュー関数の実行
HttpResponse の生成
← WSGI 境界 →
environ, start_response status, headers, iterable
この分離があるからこそ、同じ Django アプリケーションを Gunicorn から uWSGI に切り替えてもアプリケーションコードを一行も変更する必要がありません。
注釈
--timeout 30 でワーカーがタイムアウトした場合、それは Gunicorn の設定の問題であり Django のビュー関数の問題ではありません(ただし、ビュー関数の処理が30秒以上かかっていることが根本原因である可能性はあります)。
502 Bad Gateway が発生した場合の切り分けは、この境界を意識すると明確になります。
エラーコード |
原因 |
記録されるログ |
|---|---|---|
502 Bad Gateway |
Gunicorn のワーカーがタイムアウトでキルされた |
Gunicorn ログに |
500 Internal Server Error |
Django のビュー関数で未処理例外が発生した |
Django のエラーログ |
502 か 500 かという違いが、問題がどの層で起きているかを示す重要なシグナルです。
1.10.3. リバースプロキシ
Nginx や Caddy といったリバースプロキシは、Gunicorn のさらに前段に配置されます。 クライアントから見えるのはリバースプロキシだけであり、Gunicorn や Django の存在は隠蔽されます。
リバースプロキシの責務は次のとおりです。
クライアントとの接続管理(大量の同時接続のハンドリング)
TLS の終端(HTTPS の暗号化・復号化)
静的ファイルの配信
リクエストのバッファリングと転送
レスポンスの圧縮(gzip, brotli)
アクセスログの記録
レート制限や IP 制限
クライアント
│
│ HTTPS (TLS)
▼
Nginx(リバースプロキシ)
├── /static/ → ファイルシステムから直接配信
├── /media/ → ファイルシステムから直接配信
└── それ以外 → Gunicorn へ転送(HTTP or Unix socket)
│
│ HTTP (平文)
▼
Gunicorn(WSGI サーバ)
│
│ WSGI (environ, start_response)
▼
Django(アプリケーション)
Nginx と Gunicorn の間の通信は通常 HTTP(平文)または Unix ソケットで行われます。 TLS は Nginx が終端するため、Gunicorn 以降では暗号化のオーバーヘッドがありません。
ただし Django 側では request.is_secure() が False を返してしまうため、Nginx が X-Forwarded-Proto ヘッダーを付与し、Django の SECURE_PROXY_SSL_HEADER 設定でこれを認識させる必要があります。
# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# nginx.conf
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
警告
この設定がないと、Django が生成するリダイレクト URL が http:// になったり、CSRF 検証が失敗したりする問題が発生します。
クライアントの本来の IP アドレスも同様で、Nginx を経由すると REMOTE_ADDR が 127.0.0.1(Nginx のアドレス)になるため、X-Forwarded-For ヘッダーと Django の設定で元のクライアント IP を復元する必要があります。
タイムアウトの連鎖も三層の構造を意識しなければ理解できません。
層 |
設定項目 |
デフォルト値 |
|---|---|---|
Nginx |
|
60秒 |
Gunicorn |
|
30秒 |
Django ビュー |
外部 API 呼び出しタイムアウト |
アプリ依存 |
これらはそれぞれ独立した設定です。
Gunicorn のタイムアウトが Nginx のタイムアウトより短ければ、Gunicorn がワーカーをキルした後に Nginx が 502 を返します。
逆に Nginx のタイムアウトが短ければ、Gunicorn のワーカーはまだ処理中なのにクライアントには 504 Gateway Timeout が返され、ワーカーは無駄にリソースを消費し続けます。
重要
三層のタイムアウトは「外側 ≥ 内側」の関係で設定するのが原則です。
Nginx proxy_read_timeout: 60s ≥ Gunicorn --timeout: 30s ≥ 外部API timeout: 10s
この三層のタイムアウト設定を把握していれば、タイムアウト系のエラーが発生した際に「どの層が先にタイムアウトしたか」をログから特定し、適切な設定を調整できます。
三層の責務分担をまとめると、次のとおりです。
責務 |
Nginx |
Gunicorn |
Django |
|---|---|---|---|
TLS 終端 |
○ |
||
静的ファイル配信 |
○ |
||
リクエストバッファリング |
○ |
||
gzip / brotli 圧縮 |
○ |
||
アクセスログ |
○ |
○ |
|
TCP ソケット管理 |
○ |
||
HTTP パース → environ |
○ |
||
ワーカープロセス管理 |
○ |
||
タイムアウト監視 |
○ |
○ |
|
WSGIRequest 生成 |
○ |
||
ミドルウェアチェーン |
○ |
||
URL 解決 |
○ |
||
ビュー関数実行 |
○ |
||
HttpResponse 生成 |
○ |
||
CSRF / セッション / 認証 |
○ |
||
ORM / DB アクセス |
○ |
この表を頭に入れておけば、問題が発生した際に「どの列の責務か」で調査対象を絞り込めます。
Nginx のアクセスログには記録があるが Gunicorn のログにリクエストが記録されていない: Nginx と Gunicorn の間の通信(ソケット接続、バッファリング設定)を疑います。
Gunicorn のログにリクエストは記録されているが Django のログにエラーがない: Gunicorn のワーカー管理(タイムアウト、メモリ制限)を疑います。
Django のログに例外が記録されている: アプリケーションコードの問題です。
本章では Django を WSGI の視点から一貫して追跡してきました。
wsgi.py の4行から始まり、WSGIHandler の __call__、WSGIRequest への変換、URL 解決の木構造探索、ミドルウェアチェーンの構築と通過、ビュー関数の呼び出し、HttpResponse の組み立て、Django が自動で引き受ける責務、トラブルシューティングの手法、そしてサーバとの責務境界まで。
この一連の流れは、Vol.1「WSGI が生まれた背景」で書いた application(environ, start_response) が [b"Hello, World!"] を返すだけの WSGI アプリを、大規模なアプリケーション開発に耐えうる形に拡張したものです。
次章では WSGI の同期的な世界を離れ、ASGI(Asynchronous Server Gateway Interface)の仕組みを学びます。 Vol.1「WSGI が生まれた背景」で見た WSGI の限界——同期処理と WebSocket 非対応——を ASGI がどのように克服するかを、最小実装から追跡していきます。