(Django を WSGI 視点で見る)= # Django を WSGI 視点で見る 第1部では TCP ソケットから WSGI の仕様、そしてフレームワークが WSGI の上に何を積み上げているかを学びました。 第2部の最初となる本章では、Django を「WSGI アプリケーションとして」読み解きます。 普段 `python manage.py runserver` で起動している Django が、実は Vol.1「WSGI が生まれた背景」で書いた `application(environ, start_response)` とまったく同じインタフェースで動いていることを確認するところから始めましょう。 ## wsgi.py の役割 Django プロジェクトを `django-admin startproject myproject` で生成すると、`myproject/wsgi.py` というファイルが自動的に作られます。 ```python # 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` に束縛します。 ```{important} `application` は Vol.1「WSGI が生まれた背景」で学んだ WSGI の仕様に完全に準拠した callable です。 つまり `application(environ, start_response)` の形で呼び出され、バイト列のイテラブルを返します。 Django のモデル、テンプレート、ORM、管理画面、認証といった膨大な機能は、すべてこの一つの callable の内側に収まっています。 ``` ## get_wsgi_application() `get_wsgi_application()` の内部は驚くほどシンプルです。 Django のソースコード(`django/core/wsgi.py`)を確認すると、本質的には以下の処理しか行っていません。 ```python # 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` をロードする - モデルのインポートやシグナルの接続といった起動時処理を完了させる ```{note} この `setup()` はプロセスの生存期間中に一度だけ実行されます。 Gunicorn のワーカーが fork される前にこの処理が走るため、各ワーカーは初期化済みの状態で起動します。 ``` `WSGIHandler()` が返すオブジェクトこそが、Django の WSGI アプリケーション本体です。 `WSGIHandler` は `__call__` メソッドを持つクラスで、Vol.1「WSGI が生まれた背景」で関数として書いた WSGI アプリケーションをクラスベースで実装したものです。 ```python # 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__` の中で起きていることは、次の三つの段階に分かれます。 1. **environ の変換**: `environ` 辞書を `WSGIRequest` オブジェクトに変換します。Vol.1「WSGI の上に何が必要になるのか」で Werkzeug の `Request(environ)` が行っていた処理と同じ発想で、`environ["REQUEST_METHOD"]` を `request.method`、`environ["PATH_INFO"]` を `request.path` といった属性アクセスに変換します。 2. **レスポンスの取得**: `self.get_response(request)` でミドルウェアチェーンとビュー関数を通過させ、`HttpResponse` オブジェクトを得ます。 3. **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 ← バイト列イテラブル ``` ```{mermaid} flowchart LR GU[Gunicorn
WSGI サーバ] -->|environ
start_response| WH[WSGIHandler
.__call__] WH --> WR[WSGIRequest
environ 変換] WR --> GR[get_response
ミドルウェア→URL解決→ビュー] GR --> SR[start_response
status, headers] SR --> RET[return response
バイト列イテラブル] RET --> GU ``` Vol.1「WSGI が生まれた背景」で `wsgiref.simple_server` を使って自作の WSGI アプリを動かしたのと同様に、Django の `application` も任意の WSGI サーバ上で動作します。 `wsgiref` でも、Gunicorn でも、uWSGI でも、`application(environ, start_response)` を呼び出すだけで Django の全機能が起動するのは、WSGI という共通インタフェースが存在するからです。 --- 次節では `WSGIHandler.get_response()` の内部に踏み込み、Django のミドルウェアチェーンがどのように構築され、リクエストがビュー関数に届くまでにどのような処理を通過するかを追跡します。 (リクエストは Django にどう渡るか)= ## リクエストは Django にどう渡るか 前節で `WSGIHandler.__call__` が `environ` を受け取り、`WSGIRequest` に変換する流れを概観しました。 本節ではその変換処理の内部を詳しく追い、Vol.1「WSGI が生まれた背景」で学んだ `environ` 辞書のキーが Django の `request` オブジェクトのどの属性に対応するかを具体的に確認します。 ### WSGI environ から HttpRequest へ Vol.1「WSGI が生まれた背景」で見た通り、WSGI サーバはリクエストの情報を `environ` 辞書に詰めてアプリケーションに渡します。 Django の `WSGIRequest` はこの辞書を受け取り、開発者が直感的に扱える属性へ変換します。 対応関係を整理すると以下のようになります。 | environ キー | request 属性 | 内容 | |---|---|---| | `REQUEST_METHOD` | `request.method` | `"GET"`, `"POST"` など | | `PATH_INFO` | `request.path` | `"/users/42/"` | | `QUERY_STRING` | `request.GET` | `QueryDict` | | `CONTENT_TYPE` | `request.content_type` | `"application/json"` | | `CONTENT_LENGTH` | (body 読み取りに使用) | バイト数 | | `wsgi.input` | `request.body` | `bytes` | | `HTTP_HOST` | `request.headers["Host"]` | ホスト名 | | `HTTP_COOKIE` | `request.COOKIES` | `dict` | | `SERVER_NAME` | `request.META["SERVER_NAME"]` | サーバ名 | | `SERVER_PORT` | `request.META["SERVER_PORT"]` | ポート番号 | | environ 全体 | `request.META` | `dict` | ```{note} `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_` プレフィックス付きキーを逆変換しているだけで、データソースは同一です。 ### request オブジェクトの生成 `WSGIRequest` の生成過程を、Django のソースコード(`django/core/handlers/wsgi.py` および `django/http/request.py`)に沿って追跡します。 ```python # 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` からの変換)を担当します。 この分離は{numref}`Django は ASGI にどう対応しているか`({ref}`Django は ASGI にどう対応しているか`)で登場する `ASGIRequest` と対比すると明確になります。 `ASGIRequest` も同じ `HttpRequest` を継承しつつ、ASGI の `scope` と `receive` からリクエスト情報を取得します。 #### body の遅延読み取り `request.body` はプロパティとして定義されており、初回アクセス時に `wsgi.input` ストリームから読み取りが実行されます。 ```python # 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` を防いでいます。 #### GET と POST の解析 クエリ文字列とフォームデータの解析も遅延実行されます。 ```python # 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`)を解析した結果です。 ```{caution} JSON ボディは `request.POST` には入りません。 `Content-Type: application/json` で送信されたデータは `json.loads(request.body)` で明示的にパースする必要があります。 この設計は、Django が HTML フォーム送信を主な POST の用途として想定していた時代の名残です。 ``` #### ファイルアップロード `multipart/form-data` で送信されたファイルは `request.FILES` に格納されます。 Django は `Content-Length` に応じてファイルをメモリに保持するかディスクに書き出すかを `FILE_UPLOAD_MAX_MEMORY_SIZE`(デフォルト約2.5 MB)で制御します。 | サイズ | 保持先 | クラス | |---|---|---| | 小さいファイル | メモリ | `InMemoryUploadedFile` | | 大きいファイル | ディスク(一時保存) | `TemporaryUploadedFile` | #### 全体の流れ リクエストオブジェクト生成の全体像をまとめると、次の順序で処理が進みます。 ``` 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_* を逆変換 ``` ```{mermaid} flowchart TD ENV[WSGI environ] --> INIT[WSGIRequest.__init__] INIT --> META[self.META = environ] INIT --> METHOD[self.method] INIT --> PATH[self.path] INIT --> STREAM[self._stream
wsgi.input 未読み取り] STREAM -->|初回アクセス時| BODY[request.body
キャッシュ] META -->|遅延解析| GET[request.GET
QueryDict] META -->|遅延解析| POST[request.POST
body 解析] META -->|遅延解析| FILES[request.FILES
multipart 解析] ``` ```{tip} Vol.1「まずは 1 リクエストだけ処理するサーバを作る」で自作サーバに書いた `parse_request_line`、`parse_headers`、`parse_qs` は、Django の内部ではこのように `WSGIRequest` と `HttpRequest` のプロパティに分散して実装されています。 抽象化の層は増えていますが、やっていることの本質は変わりません。 ``` --- 次節では、生成された `request` オブジェクトがミドルウェアチェーンをどのように通過し、URL 解決を経てビュー関数に届くかを追跡します。 (URL 解決の流れ)= ## URL 解決の流れ 前節で `WSGIRequest` オブジェクトが生成されるまでを追いました。 本節では、そのリクエストが持つパス情報(`request.path_info`)をもとに、Django がどのビュー関数を呼び出すかを決定する「URL 解決(URL resolution)」の仕組みを内部から追跡します。 ### URLconf Django の URL 解決はすべて、プロジェクトの **ルート URLconf** から始まります。 `settings.py` の `ROOT_URLCONF` で指定されたモジュール(通常は `myproject/urls.py`)が起点です。 ```python # myproject/settings.py ROOT_URLCONF = 'myproject.urls' ``` ```python # 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')), ] ``` ```python # users/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.user_list, name='user-list'), path('/', views.user_detail, name='user-detail'), path('/posts/', views.user_posts, name='user-posts'), ] ``` `urlpatterns` はリストですが、単なる文字列のリストではありません。 各 `path()` や `include()` は内部で `URLPattern` または `URLResolver` オブジェクトを生成します。 | クラス | 役割 | |---|---| | `URLPattern` | 末端のビュー関数へのマッピング | | `URLResolver` | 子の `urlpatterns` を持つ中間ノード | つまり `urlpatterns` 全体は木構造を形成しており、Django はこの木をルートから順にたどってマッチするパターンを探します。 ``` URLResolver (root: "") ├─ URLResolver ("admin/") → admin.site.urls ├─ URLResolver ("api/users/") → users.urls │ ├─ URLPattern ("") → views.user_list │ ├─ URLPattern ("/") → views.user_detail │ └─ URLPattern ("/posts/") → views.user_posts └─ URLResolver ("api/articles/") → articles.urls └─ ... ``` ```{note} この木構造はプロセス起動時に一度だけ構築されます。以降のリクエストでは構築済みの木を探索するだけなので、`include()` を使ってアプリケーションごとに `urls.py` を分割しても、最終的には単一の木に統合されます。 ``` ```{mermaid} flowchart TD ROOT[URLResolver
root] --> ADMIN[URLResolver
admin/] ROOT --> USERS[URLResolver
api/users/] ROOT --> ARTICLES[URLResolver
api/articles/] USERS --> UL[URLPattern
空文字
→ user_list] USERS --> UD[URLPattern
<int:user_id>/
→ user_detail] USERS --> UP[URLPattern
<int:user_id>/posts/
→ user_posts] ``` ### path と re_path `path()` と `re_path()` は URL パターンを定義する二つの方法です。 ```python from django.urls import path, re_path # path() — シンプルな構文、型変換付き path('users//', views.user_detail) # re_path() — 正規表現による柔軟なマッチング re_path(r'^users/(?P[0-9]+)/$', views.user_detail) ``` `path()` は Django 2.0 で導入された構文で、`<型:名前>` 形式のパスコンバータを使います。 Django が標準で提供するコンバータは次の5種類です。 | コンバータ | 変換後の型 | マッチ対象 | |---|---|---| | `str` | `str` | スラッシュ以外の文字列(デフォルト) | | `int` | `int` | 0 以上の整数 | | `slug` | `str` | スラッグ文字列 | | `uuid` | `UUID` | UUID 形式 | | `path` | `str` | スラッシュを含む文字列 | 内部的には、各コンバータはパターンを正規表現に変換しています。 たとえば `` は `(?P[0-9]+)` に変換されます。 つまり `path()` は `re_path()` の構文糖であり、最終的に同じ正規表現マッチングエンジンで処理されます。 `path()` には重要な追加機能として、マッチした文字列を自動的に Python の型に変換する機能があります。 `` でマッチした `"42"` は、ビュー関数に渡される前に `int("42")` → `42` に変換されます。 `re_path()` は正規表現をそのまま記述できるため、より複雑なパターン(例: 日付形式 `(?P[0-9]{4})/(?P[0-9]{2})/`)に対応できますが、型変換は行われません。 マッチ結果は常に文字列としてビュー関数に渡されます。 ### resolver match URL 解決が実行されるタイミングは、ミドルウェアの処理中です。 Django の `WSGIHandler.get_response()` から呼ばれるミドルウェアチェーンの中で、`request.path_info` に対して `URLResolver.resolve()` が実行されます。 ```python # 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` の中で `""` はマッチせず、`"/"` が `"42/"` にマッチします。 この時点で `ResolverMatch` オブジェクトが生成されます。 ```python ResolverMatch( func=views.user_detail, args=(), kwargs={"user_id": 42}, # int に変換済み url_name="user-detail", ) ``` マッチ結果は `request.resolver_match` に格納されます。 どのパターンにもマッチしなかった場合は `Resolver404` 例外が送出され、Django はこれを捕捉して 404 レスポンスに変換します。 ```{note} Vol.1「本書の対象読者とゴール」で「404 はどこで発生するか」を整理した際に挙げた「フレームワーク層での 404」がまさにこのケースです。 ``` ### path parameter の受け渡し `ResolverMatch` に格納された `args` と `kwargs` は、ビュー関数の引数としてそのまま渡されます。 ```python # 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)` が呼び出されます。 ビュー関数側では以下のように受け取ります。 ```python # 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` → ビュー呼び出しの流れで実現しています。 ```{warning} パスパラメータの名前はビュー関数の仮引数名と一致している必要があります。 `` と定義した場合、ビュー関数は `def user_detail(request, user_id)` でなければなりません。 名前が一致しないと `TypeError` が発生し、Django はこれを 500 エラーとして処理します。 ``` クラスベースビュー(CBV)の場合も本質は同じです。 `as_view()` が返す関数が呼び出され、内部で `self.kwargs` にパスパラメータが格納されます。 ```python # クラスベースビューでのパスパラメータ取得 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 解決で特定されたビュー関数が実際に呼び出される前後に介入する「ミドルウェア」の仕組みを追跡します。 (middleware chain の流れ)= ## middleware chain の流れ 前節で URL 解決によりビュー関数が特定されるまでを追いました。 しかし Django では、リクエストがビューに届く前にも、レスポンスがクライアントに返る前にも、複数の処理層を通過します。 それがミドルウェアです。 本節では Django のミドルウェアチェーンがどのように構築され、リクエストとレスポンスがどの順序で流れるかを内部構造から追跡します。 ### リクエスト前処理 Django の `settings.py` には `MIDDLEWARE` というリストが定義されています。 ```python # 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', ] ``` ```{important} このリストの順序は極めて重要です。 Django はサーバ起動時にこのリストを **末尾から先頭へ** 逆順に処理し、各ミドルウェアを入れ子にしたチェーンを構築します。 ``` 概念的には次のような構造になります。 ```text # 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 ← レスポンス ``` 各ミドルウェアの基本構造は以下の通りです。 ```python 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)」の仕組みにより、不正なリクエストを早期に遮断できます。 ### ビュー前後処理 Django のミドルウェアには `__call__` 以外にも、ビュー関数の前後に介入するフックメソッドが用意されています。 ```python 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` デコレータが付いている場合は検証をスキップします。 処理の順序を時系列で整理すると次のようになります。 1. `__call__` のリクエスト前処理(外側から内側へ)— `SecurityMiddleware` → `SessionMiddleware` → ... → `XFrameOptionsMiddleware` 2. URL 解決(`request.path_info` → `ResolverMatch`) 3. `process_view`(外側から内側へ)— `SecurityMiddleware.process_view` → ... → `XFrameOptionsMiddleware.process_view` 4. ビュー関数の実行 5. `process_template_response`(内側から外側へ、`TemplateResponse` の場合のみ) 6. `__call__` のレスポンス後処理(内側から外側へ)— `XFrameOptionsMiddleware` → ... → `SessionMiddleware` → `SecurityMiddleware` ```{mermaid} sequenceDiagram participant C as クライアント participant S as Security MW participant Se as Session MW participant A as Auth MW participant V as ビュー関数 C->>S: リクエスト S->>Se: process_request Se->>A: process_request A->>V: URL解決 → process_view V->>A: HttpResponse A->>Se: process_response Se->>S: process_response S->>C: レスポンス ``` ### レスポンス後処理 ビュー関数が `HttpResponse` を返した後、レスポンスはミドルウェアチェーンを内側から外側へ逆順に通過します。 `self.get_response(request)` の戻り値として受け取った `response` オブジェクトを、各ミドルウェアが検査・加工します。 ```python 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 の設定前にセキュリティヘッダーの処理が終了してしまいます。 ### 例外処理との関係 ビュー関数やミドルウェアの処理中に例外が発生した場合、Django は `process_exception` フックを通じて例外を処理します。 ```python 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 ステータス | |---|---| | `Resolver404` | 404 Not Found | | `PermissionDenied` | 403 Forbidden | | `SuspiciousOperation` | 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) │ ▼ レスポンス後処理(通常通り内側 → 外側) ``` ```{caution} `process_exception` はビュー関数の例外に対してのみ呼ばれます。 ミドルウェア自身の `__call__` 内で発生した例外は、そのミドルウェアの外側のミドルウェアの `__call__` に伝播するため、`process_exception` ではなく Python の通常の例外伝播メカニズムで処理されます。 これが「ミドルウェアの順序が重要」というもう一つの理由です。 最も外側のミドルウェアで例外が発生すると、他のミドルウェアのレスポンス後処理が一切実行されず、セッションの保存やヘッダーの付与が行われないまま生のエラーが返される可能性があります。 ``` --- ミドルウェアチェーンの構造を理解すると、Vol.1「本書の対象読者とゴール」で整理した「ログはどこに出るか」「エラーはどの層で発生するか」という問いに対して、より正確な回答が可能になります。 次節では、ビュー関数が返した `HttpResponse` が WSGI サーバを経由してクライアントに届くまでの最後の工程を追跡します。 (View の実行)= ## View の実行 前節でミドルウェアチェーンを通過したリクエストが、URL 解決で特定されたビュー関数に到達するまでを追いました。 本節では、Django がビュー関数を実際に呼び出す仕組みを、関数ベースビュー(FBV)とクラスベースビュー(CBV)の両方について内部から追跡します。 ### 関数ベースビュー(FBV) 関数ベースビューは Django の最もシンプルなビュー実装です。 `request` オブジェクトを第一引数として受け取り、`HttpResponse` を返す関数がそのままビューになります。 ```python # 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` にたどり着きます。 ```python # 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` 関数を書いたときの感覚と本質的に同じです。 ``` ### クラスベースビュー(CBV) クラスベースビューは、ビューのロジックをクラスのメソッドとして構造化する仕組みです。 ```python # 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()` の戻り値を登録します。 ```python # users/urls.py from django.urls import path from .views import UserDetailView urlpatterns = [ path('/', UserDetailView.as_view(), name='user-detail'), ] ``` ここで疑問が生じます。Django の URL 解決は callable を期待しているのに、クラスをどうやってビュー関数として扱うのでしょうか。 答えは `as_view()` にあります。 ```python # 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 解決から見れば、クラスベースビューであっても結局は「関数が呼ばれる」という構造に変わりありません。 ```{important} `view` 関数が呼ばれるたびに `cls(**initkwargs)` でクラスの **新しいインスタンス** が生成されます。 リクエスト間でインスタンスが共有されないため、`self` に状態を保持しても他のリクエストに影響しません。 これは Gunicorn のマルチワーカー環境でも安全に動作するための設計です。 ``` ### dispatch の流れ `as_view()` が生成した `view` 関数の最後で `self.dispatch()` が呼ばれます。 `dispatch` はクラスベースビューの中核であり、HTTP メソッドに応じて適切なハンドラメソッドを呼び分けます。 ```python # 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` の処理を順に追います。 1. `request.method` を小文字に変換します(例: `"GET"` → `"get"`)。 2. `http_method_names` リストに含まれるかを確認します。 3. 含まれていれば `getattr(self, method)` でインスタンスのメソッドを取得します。`UserDetailView` に `get` メソッドが定義されていれば `self.get` が、`delete` メソッドがあれば `self.delete` が取得されます。 4. メソッドが定義されていない場合(たとえば 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({...}) ``` ```{mermaid} flowchart TD AV[as_view 呼び出し] --> VF[view 関数
リクエストごとに実行] VF --> CI[cls インスタンス生成] CI --> SET[self.request, args, kwargs 設定] SET --> DI[dispatch] DI --> ML[method = request.method.lower] ML --> GA{getattr self method} GA -->|存在する| HM[self.get / self.post など] GA -->|存在しない| NA[http_method_not_allowed
→ 405] HM --> RES[JsonResponse] ``` Django が提供する汎用ビュー(`ListView`, `DetailView`, `CreateView` など)はすべてこの `View.dispatch` を継承しています。 たとえば `DetailView` は `get` メソッドの中でオブジェクトの取得とテンプレートレンダリングを行い、`CreateView` は `get` でフォーム表示、`post` でフォーム処理を行います。 いずれも `dispatch` による HTTP メソッドの振り分けという基盤の上に成り立っています。 ### request / args / kwargs ビュー関数に渡される引数を改めて整理します。 関数ベースビューでもクラスベースビューでも、渡される情報は同じ三種類です。 - **`request`**: {numref}`リクエストは Django にどう渡るか`({ref}`リクエストは Django にどう渡るか`)で詳しく見た `WSGIRequest` のインスタンスです。`request.method`, `request.path`, `request.GET`, `request.body`, `request.headers`, `request.user`(ミドルウェアが設定)などの属性を持ちます。 - **`args`(位置引数)**: `re_path` で名前なしグループ `([0-9]+)` を使った場合に文字列として渡されます。`path()` を使っている場合、位置引数は通常空タプルです。 - **`kwargs`(キーワード引数)**: `path('/', ...)` で定義したパスパラメータが `user_id=42` のように渡されます。パスコンバータによる型変換は URL 解決の段階で完了しているため、ビュー関数が受け取る時点で `int` 型になっています。 ```python # 関数ベースビュー: 直接引数として受け取る 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 インタフェースを通じてサーバへ渡され、最終的にクライアントに届くまでのレスポンス返却処理を追跡します。 (ch06-レスポンス生成)= ## レスポンス生成 前節でビュー関数が呼び出されるまでの経路を追いました。 本節では、ビュー関数が返す `HttpResponse` オブジェクトの内部構造を解剖し、Django がどのようにしてレスポンスを組み立て、最終的に WSGI インタフェースへ渡すかを追跡します。 Vol.1「まずは 1 リクエストだけ処理するサーバを作る」で自作した `make_response()` 関数がステータスライン・ヘッダー・ボディを手動で組み立てていた処理を、Django がどのように抽象化しているかを見ていきましょう。 ### HttpResponse `HttpResponse` は Django のレスポンス体系の基底クラスです。 すべてのレスポンスクラスはこのクラスを継承しています。 ```python from django.http import HttpResponse # 最もシンプルな使い方 response = HttpResponse("Hello, Django!") response = HttpResponse("

Welcome

", content_type="text/html; charset=utf-8") response = HttpResponse(status=204) ``` 内部構造をソースコード(`django/http/response.py`)に沿って追うと、`HttpResponse` は驚くほど素朴な仕組みで動いています。 ```python # 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__` の中で行われます。 ```python # 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` のサブクラスとして、ステータスコード固定のショートカットクラスも提供しています。 | クラス | ステータスコード | |---|---| | `HttpResponseNotFound` | 404 | | `HttpResponseForbidden` | 403 | | `HttpResponseRedirect` | 302 | | `HttpResponsePermanentRedirect` | 301 | | `HttpResponseNotAllowed` | 405 | | `HttpResponseServerError` | 500 | いずれも `HttpResponse` を継承し、`status_code` クラス変数を上書きしているだけです。 ### JsonResponse REST API を構築する際に頻繁に使う `JsonResponse` は、`HttpResponse` を継承した薄いラッパーです。 ```python from django.http import JsonResponse # 辞書を渡す response = JsonResponse({"id": 42, "name": "Taro", "email": "taro@example.com"}) # リストを渡す場合は safe=False が必要 response = JsonResponse([{"id": 1}, {"id": 2}], safe=False) # ステータスコードの指定 response = JsonResponse({"error": "Not Found"}, status=404) ``` 内部実装は以下の通りです。 ```python # 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` の指定を求めることで、開発者に意図を確認させています。 ```{note} `DjangoJSONEncoder` は Python 標準の `json.JSONEncoder` を拡張し、`datetime`, `date`, `time`, `Decimal`, `UUID` などの型を自動的にシリアライズできるようにしています。 ORM から取得したモデルインスタンスを直接渡すことはできないため、`values()` や手動の辞書構築、あるいは Django REST Framework のシリアライザを使う必要があります。 ``` ### TemplateResponse `TemplateResponse` は、テンプレートのレンダリングを遅延実行するレスポンスクラスです。 ```python 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` ショートカットとの違いは、レスポンスが返される時点ではまだテンプレートのレンダリングが実行されていないという点です。 ```python # 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__ フロー) ``` ```{mermaid} flowchart LR VF[ビュー関数] -->|HttpResponse| MC[ミドルウェア
process_response] VF -->|TemplateResponse
未レンダリング| TR[process_template_response] TR --> RE[response.render
テンプレート処理] RE --> MC MC -->|send| WS[WSGIHandler
start_response + return] WS --> GU[Gunicorn
クライアントへ] ``` 一方、`render(request, template_name, context)` ショートカットは即座にテンプレートをレンダリングし、通常の `HttpResponse` を返します。 ミドルウェアでテンプレートやコンテキストを操作する必要がない場合は `render` の方がシンプルです。 ### ストリーミングレスポンス 大容量のデータをレスポンスとして返す場合、全体をメモリ上に構築してから送信するのは非効率です。 `StreamingHttpResponse` はジェネレータを受け取り、チャンク単位でクライアントに送信します。 ```python 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` とは本質的に異なります。 ```python # 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 アプリ」の仕組みがそのまま活かされています。 ```{caution} `StreamingHttpResponse` にはいくつかの制約があります。 - ボディ全体が確定していないため `Content-Length` ヘッダーが設定できず、`Transfer-Encoding: chunked` に依存します。 - `content` プロパティへのアクセスはできません(イテレータは一度しかイテレートできないため)。 - ミドルウェアの中には `response.content` を参照するものがあり、そうしたミドルウェアとの併用には注意が必要です。 ``` `FileResponse` は `StreamingHttpResponse` のサブクラスで、ファイルオブジェクトの送信に特化しています。 ```python 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 相当 | |---|---| | `HttpResponse` | `make_response` 関数 | | `JsonResponse` | `json_response` ヘルパー | | `StreamingHttpResponse` | WSGI のジェネレータ返却 | 抽象化の層は厚くなっていますが、WSGI の「ステータスとヘッダーを `start_response` で送り、ボディをイテラブルで返す」という基本構造は変わっていません。 次節では、Django の WSGI 処理全体を振り返り、トラブルシューティングの観点から各工程を総括します。 (Django が面倒を見てくれているもの)= ## Django が面倒を見てくれているもの ここまでの節で、リクエストが `WSGIHandler` に届き、`WSGIRequest` に変換され、ミドルウェアチェーンを通過し、URL 解決を経てビュー関数が呼ばれ、`HttpResponse` が返されるまでの一連の流れを追いました。 この過程で何度も「ミドルウェアが処理する」「Django が自動的に変換する」と記述してきましたが、本節ではそれらの自動処理を横断的に整理します。 普段ビュー関数を書くだけでは意識しない、Django が裏側で引き受けている責務の全体像を把握することが目的です。 ### CSRF Cross-Site Request Forgery(CSRF)は、ユーザーが意図しないリクエストを、認証済みのセッションを悪用して送信させる攻撃です。 Django の `CsrfViewMiddleware` はこの攻撃に対する防御をほぼ完全に自動化しています。 仕組みを内側から見ると、処理は次の二つの段階に分かれます。 - **GET リクエスト時**: テンプレート内の `{% csrf_token %}` タグが hidden フィールドとしてトークンを埋め込み、同時にレスポンスの `Set-Cookie` ヘッダーで同じトークンを Cookie にも保存します。 - **POST リクエスト時**: `CsrfViewMiddleware` の `process_view` フックがフォームから送信されたトークンと Cookie のトークンを比較し、一致しなければ 403 を返します。 ```{mermaid} sequenceDiagram participant B as ブラウザ participant D as Django B->>D: GET /form/ D->>B: HTML + csrftoken Cookie + hidden field B->>D: POST /form/ (csrfmiddlewaretoken + Cookie) D->>D: CsrfViewMiddleware: トークン比較 alt 一致 D->>B: 200 OK else 不一致 D->>B: 403 Forbidden end ``` ```python # テンプレート側
{% csrf_token %}
``` ```html
``` REST API でセッション認証を使わず、トークン認証や JWT で保護する場合、CSRF 保護は不要です。 その場合はビュー関数に `@csrf_exempt` デコレータを付与するか、Django REST Framework がデフォルトで `SessionAuthentication` 以外の認証クラスに対して CSRF チェックを無効化する仕組みを利用します。 ```{note} `CsrfViewMiddleware` が `process_view` フックを使っているのには設計上の理由があります。 `process_view` はビュー関数が特定された後に呼ばれるため、ビューに `@csrf_exempt` が付いているかどうかを確認できます。 リクエスト前処理の段階(`__call__` の前半)ではビュー関数が未確定であるため、この判断ができません。 ミドルウェアフックの使い分けが、具体的な機能実装に直結している好例です。 ``` ### セッション HTTP はステートレスなプロトコルであり、連続するリクエスト間でユーザーの状態を保持する仕組みは HTTP 自体には存在しません。 Django の `SessionMiddleware` はこの問題を、Cookie とサーバサイドのストレージを組み合わせて解決しています。 リクエスト前処理の段階で、`SessionMiddleware` は `Cookie` ヘッダーからセッション ID(デフォルトでは `sessionid`)を取得し、そのIDに紐づくセッションデータをストレージ(データベース、キャッシュ、ファイルなど)から読み込みます。読み込まれたデータは `request.session` として辞書ライクなオブジェクトで提供されます。 ```python 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` に依存しているためです。 セッションが利用可能になっていなければ、ユーザーの認証状態を復元できません。 ``` ### 認証 `AuthenticationMiddleware` は `SessionMiddleware` が設定した `request.session` からユーザー ID を取得し、データベースからユーザーオブジェクトを復元して `request.user` に設定します。 ```python # 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` を参照しなければクエリは発生しません。 これは{numref}`リクエストは Django にどう渡るか`({ref}`リクエストは 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` の順序依存は必然的なものです。 ### 例外ページ Django はビュー関数やミドルウェアで発生した例外を自動的に HTTP レスポンスに変換します。 この仕組みは `BaseHandler._get_response` の内部と、前節で解説した `process_exception` フックの組み合わせで実現されています。 例外の種類と対応するレスポンスの関係を整理すると次のとおりです。 | 例外クラス | HTTP ステータス | |---|---| | `Http404`, `Resolver404` | 404 Not Found | | `PermissionDenied` | 403 Forbidden | | `SuspiciousOperation`(CSRF 検証失敗を含む) | 400 Bad Request | | その他の未処理例外 | 500 Internal Server Error | `DEBUG` 設定により例外ページの内容が劇的に変わります。 - **`DEBUG=True`**: Django は詳細なスタックトレース、ローカル変数の値、`request.META` の内容、SQL クエリの履歴を含む HTML ページを生成します。開発中はこのページが強力なデバッグツールになります。 - **`DEBUG=False`**: `handler404`, `handler500` などで定義されたカスタムテンプレート(または Django デフォルトの簡素なページ)が表示されます。 ```python # urls.py でカスタムエラーハンドラを設定 handler404 = 'myproject.views.custom_404' handler500 = 'myproject.views.custom_500' ``` ```{danger} 本番環境で `DEBUG=True` を有効にしてはいけません。 例外ページにはデータベースの接続情報、シークレットキー、環境変数、ファイルパスなど、攻撃者にとって有用な情報が大量に含まれます。 Vol.1「WSGI の上に何が必要になるのか」で触れた Werkzeug のインタラクティブデバッガと同様、開発用の便利機能は本番では致命的なセキュリティホールになります。 ``` ### 設定管理 Django の `settings.py` はフレームワーク全体の挙動を制御する単一の設定ファイルです。 ミドルウェアの構成、データベース接続、テンプレートエンジン、静的ファイルの配信、セキュリティ関連のフラグなど、数百の設定項目が一箇所に集約されています。 {numref}`Django を WSGI 視点で見る`({ref}`Django を WSGI 視点で見る`)で見た通り、`wsgi.py` の `os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')` により、どの設定ファイルを読み込むかが決定されます。 `django.setup()` の実行時に設定が読み込まれ、`django.conf.settings` というグローバルオブジェクトを通じてプロジェクト全体からアクセス可能になります。 ```python 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` のように設定ファイルを分割し、共通部分はベースファイルからインポートするパターンが一般的です。 この仕組みがあるため、同じコードベースを異なる環境にデプロイする際にアプリケーションコードを変更する必要がありません。 セキュリティ上特に重要な設定項目とその影響は次のとおりです。 | 設定項目 | 用途 | 誤設定時のリスク | |---|---|---| | `SECRET_KEY` | セッション署名・CSRF トークン生成 | 漏洩するとセッションの偽造が可能 | | `ALLOWED_HOSTS` | 許可するホスト名のリスト | `["*"]` のままだと HTTP Host ヘッダー攻撃に脆弱 | | `DEBUG` | デバッグモードの有効化 | `True` のまま本番稼動すると情報漏洩が発生 | Django は `DEBUG=False` かつ `ALLOWED_HOSTS` が未設定の場合に起動時エラーを出すなど、危険な設定を検出する仕組みも備えています。 --- 本節で整理した CSRF 防御、セッション管理、認証、例外ハンドリング、設定管理は、いずれもビュー関数のコードには直接現れません。 開発者が `request.user.is_authenticated` と書くだけでユーザー認証が判定でき、POST フォームに `{% csrf_token %}` を入れるだけで CSRF 攻撃を防げるのは、これらの仕組みがミドルウェアと設定システムに組み込まれているからです。 しかしトラブルが発生したとき、たとえば「ログインしているはずなのに `request.user` が `AnonymousUser` になる」「CSRF 検証が失敗する」「本番で詳細なエラーページが表示される」といった問題に直面したとき、これらの内部構造を知っていることが解決への最短経路になります。 次節では{numref}`Django を WSGI 視点で見る`({ref}`Django を WSGI 視点で見る`)全体を振り返り、トラブルシューティングの観点で各工程を総括します。 (ch06-トラブルシューティングの観点)= ## トラブルシューティングの観点 {numref}`Django を WSGI 視点で見る`({ref}`Django を WSGI 視点で見る`)を通じて Django の WSGI ハンドラからビュー関数、レスポンス生成までの内部構造を追跡してきました。 本節ではその知識を実際の問題解決に応用します。 ここで取り上げる5つのケースはいずれも「Django のコードは正しく書いたはずなのに動かない」という状況で頻出するものであり、内部構造を知らなければ原因の特定に時間を要する問題ばかりです。 ### 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` で結合されるパスは先頭にスラッシュを付けないのが原則です。 ```text # NG: 先頭スラッシュ path('/detail/', views.user_detail) # → /api/users//detail/ になる可能性 # OK: 先頭スラッシュなし path('detail/', views.user_detail) # → /api/users/detail/ ``` **3. パスコンバータの型不一致** `path('/', ...)` に対して `/users/abc/` がリクエストされると、`int` への変換が失敗し、そのパターンはマッチしません。 全パターンがマッチしなければ 404 になります。 エラーメッセージは「型変換に失敗した」ではなく単に「ページが見つからない」なので、パスコンバータの型を見直す必要があります。 **4. 末尾スラッシュの不一致** Django はデフォルトで `APPEND_SLASH=True` が設定されており、`CommonMiddleware` が末尾スラッシュのないリクエストを自動的にリダイレクトします。 ただしこのリダイレクトは `urlpatterns` にスラッシュ付きのパターンが存在する場合にのみ発生します。 ```{caution} POST リクエストに対して 301 リダイレクトが発生すると、リダイレクト先が GET になるためデータが消失する可能性があります。 API では末尾スラッシュの有無を統一するか、`APPEND_SLASH=False` に設定して明示的に管理するのが安全です。 ``` 原因の切り分けには `django.urls.resolve` を対話型シェルで使うのが効果的です。 ```python 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.") ``` ### ミドルウェアの順序問題 {numref}`middleware chain の流れ`({ref}`middleware chain の流れ`)で解説した通り、`MIDDLEWARE` リストの順序は依存関係を反映しています。 順序を誤ると、一見すると無関係に見えるエラーが発生します。 典型的なのは `AuthenticationMiddleware` を `SessionMiddleware` より前に配置したケースです。 `AuthenticationMiddleware` は `request.session` からユーザー ID を取得しますが、`SessionMiddleware` がまだ実行されていなければ `request.session` が存在せず、`AttributeError` や `ImproperlyConfigured` が発生します。 ```text # 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__` の先頭でログを出力する方法が有効です。 ```python 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` にどの属性が設定されているかを確認できます。 ### request body の扱い Django の `request.body` に関するトラブルは、{numref}`リクエストは Django にどう渡るか`({ref}`リクエストは Django にどう渡るか`)で解説した遅延読み取りとストリームの性質に起因するものが大半です。 最も多いのは、ミドルウェアで `request.body` を読み取った後、ビュー関数でも読み取ろうとするケースです。 Django の `HttpRequest` は `body` プロパティにキャッシュ機構を持っているため、通常は二回目のアクセスもキャッシュから返されます。 しかしカスタムミドルウェアが `request.read()` や `request._stream.read()` を直接呼んだ場合、キャッシュが効かずにストリームが消費され、ビュー関数側で空のボディが返されることがあります。 ```text # 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)` で明示的にパースする必要があります。 ```{warning} フォーム送信に慣れた開発者が `request.POST` を参照して空の `QueryDict` を受け取り困惑するのはよくある光景です。 JSON ボディは `request.POST` ではなく `request.body` から取得してください。 ``` ```python # フォームデータ(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` の読み取り方を理解していれば、この問題の原因を推測できます。 ### ALLOWED_HOSTS 本番環境で最も頻出する Django 固有のエラーが `DisallowedHost` です。 `DEBUG=False` の環境で `ALLOWED_HOSTS` にリクエストのホスト名が含まれていないと、Django は 400 Bad Request を返します。 ```python # 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` に何を追加すべきかが分かります。 ```python # settings.py(ロギング設定) LOGGING = { 'version': 1, 'handlers': { 'console': {'class': 'logging.StreamHandler'}, }, 'loggers': { 'django.security.DisallowedHost': { 'handlers': ['console'], 'level': 'DEBUG', }, }, } ``` ```{warning} 開発中に `ALLOWED_HOSTS = ["*"]` と設定するのは便利ですが、この設定を本番にデプロイすると Host ヘッダー攻撃に脆弱になります。 Django の `check --deploy` コマンドはこうした危険な設定を検出してくれます。 ``` ### 静的ファイル配信の誤解 「開発サーバでは CSS や JavaScript が表示されるのに、本番にデプロイしたら表示されなくなった」は Django 初学者が必ず遭遇する問題です。 これは Django の静的ファイル配信の設計思想を理解していないことに起因します。 - **開発環境(`DEBUG=True`)**: `python manage.py runserver` の開発サーバは `django.contrib.staticfiles` が自動的に静的ファイルを配信します。各アプリの `static/` ディレクトリからファイルを探し、`/static/css/style.css` のようなリクエストに対して直接レスポンスを返します。 - **本番環境(`DEBUG=False`)**: Django は静的ファイルを一切配信しません。 ```{important} これは意図的な設計です。Django は Python のアプリケーションサーバであり、静的ファイルの配信は Nginx や CDN が担うべき責務です。 Vol.1「本書の対象読者とゴール」で整理した「役者」の分担で言えば、静的ファイルはリバースプロキシ層の仕事です。 ``` 本番環境での正しい手順は次のとおりです。 1. `python manage.py collectstatic` を実行して全アプリの静的ファイルを `STATIC_ROOT` ディレクトリに集約します。 2. Nginx がそのディレクトリを直接配信するように設定します。 ```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 ストリームの性質 | | `ALLOWED_HOSTS` | HTTP Host ヘッダーの検証 | | 静的ファイル配信の誤解 | サーバ層とアプリ層の責務分担 | Vol.1「HTTP は何をやりとりしているのか」から Vol.1「WSGI の上に何が必要になるのか」で積み上げた低レイヤの知識と、本章で追跡した Django の内部構造が、これらの問題を「見える」ものに変えてくれます。 次節では、本章の締めくくりとして Django とその外側にあるサーバ群の責務境界を明確にします。「この問題は Django を直すべきか、それともサーバ設定の問題か」という判断を正確に下せるようになることが目的です。 (どこまでが Django の責務で、どこからがサーバの責務か)= ## どこまでが Django の責務で、どこからがサーバの責務か {numref}`Django を WSGI 視点で見る`({ref}`Django を WSGI 視点で見る`)の締めくくりとして、Django とその外側にあるサーバ群の責務境界を明確にします。 Vol.1「本書の対象読者とゴール」で「役者を整理する」として6つのコンポーネントを概観しましたが、本節ではそれを Django の実装に即して具体化します。 「この問題は Django のコードを直すべきか、サーバの設定を変えるべきか、Nginx の設定を見るべきか」という判断を正確に下せるようになることが目的です。 ### 開発サーバ `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 を直接提供できません | | 静的ファイル最適化なし | `DEBUG=True` でのみ有効であり、キャッシュヘッダーや圧縮も行われません | ```{tip} 開発サーバの存在意義は、Gunicorn や Nginx の設定なしにコードの動作確認ができるという一点に集約されます。 `runserver` で動くアプリケーションは、そのまま Gunicorn でも動きます。 なぜなら両者は同じ WSGI インタフェースで Django の `application` callable を呼び出しているからです。 ``` ### 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)` を呼び出します。 ```bash 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 に切り替えてもアプリケーションコードを一行も変更する必要がありません。 ```{note} `--timeout 30` でワーカーがタイムアウトした場合、それは Gunicorn の設定の問題であり Django のビュー関数の問題ではありません(ただし、ビュー関数の処理が30秒以上かかっていることが根本原因である可能性はあります)。 ``` 502 Bad Gateway が発生した場合の切り分けは、この境界を意識すると明確になります。 | エラーコード | 原因 | 記録されるログ | |---|---|---| | 502 Bad Gateway | Gunicorn のワーカーがタイムアウトでキルされた | Gunicorn ログに `[CRITICAL] WORKER TIMEOUT` | | 500 Internal Server Error | Django のビュー関数で未処理例外が発生した | Django のエラーログ | 502 か 500 かという違いが、問題がどの層で起きているかを示す重要なシグナルです。 ### リバースプロキシ 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(アプリケーション) ``` ```{mermaid} flowchart LR CL[クライアント] -->|HTTPS| NX[Nginx
リバースプロキシ] NX -->|/static/
/media/| FS[ファイルシステム
直接配信] NX -->|それ以外
HTTP| GU[Gunicorn
WSGI サーバ] GU -->|environ
start_response| DJ[Django
アプリケーション] ``` Nginx と Gunicorn の間の通信は通常 HTTP(平文)または Unix ソケットで行われます。 TLS は Nginx が終端するため、Gunicorn 以降では暗号化のオーバーヘッドがありません。 ただし Django 側では `request.is_secure()` が `False` を返してしまうため、Nginx が `X-Forwarded-Proto` ヘッダーを付与し、Django の `SECURE_PROXY_SSL_HEADER` 設定でこれを認識させる必要があります。 ```python # settings.py SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') ``` ```nginx # 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; } ``` ```{warning} この設定がないと、Django が生成するリダイレクト URL が `http://` になったり、CSRF 検証が失敗したりする問題が発生します。 クライアントの本来の IP アドレスも同様で、Nginx を経由すると `REMOTE_ADDR` が `127.0.0.1`(Nginx のアドレス)になるため、`X-Forwarded-For` ヘッダーと Django の設定で元のクライアント IP を復元する必要があります。 ``` タイムアウトの連鎖も三層の構造を意識しなければ理解できません。 | 層 | 設定項目 | デフォルト値 | |---|---|---| | Nginx | `proxy_read_timeout` | 60秒 | | Gunicorn | `--timeout` | 30秒 | | Django ビュー | 外部 API 呼び出しタイムアウト | アプリ依存 | これらはそれぞれ独立した設定です。 - Gunicorn のタイムアウトが Nginx のタイムアウトより短ければ、Gunicorn がワーカーをキルした後に Nginx が 502 を返します。 - 逆に Nginx のタイムアウトが短ければ、Gunicorn のワーカーはまだ処理中なのにクライアントには 504 Gateway Timeout が返され、ワーカーは無駄にリソースを消費し続けます。 ```{important} 三層のタイムアウトは「外側 ≥ 内側」の関係で設定するのが原則です。 ``` ``` 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 がどのように克服するかを、最小実装から追跡していきます。