(WSGI の上に何が必要になるのか)= # WSGI の上に何が必要になるのか {numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)では、WSGI の仕様を学び、最小限の WSGI アプリケーションを自分の手で書きました。 `environ` からリクエスト情報を取り出し、`start_response` でステータスとヘッダーを返し、イテラブルでボディを返す——WSGI の仕組みは理解できましたが、同時に「これをそのまま使い続けるのはつらい」という感覚も得たのではないでしょうか。 本章では、その「つらさ」を具体的に整理します。 生の WSGI で何が不便なのかを明確にすることで、フレームワークが何を解決しているのかが見えてきます。 フレームワークの便利な機能をただ使うのではなく、「なぜこの機能が存在するのか」を理解することが、本章の目的です。 ## 生の WSGI がつらい理由 前章の JSON API の例を振り返ってみましょう。 ユーザー一覧を返すだけのエンドポイントでも、こんなコードを書く必要がありました。 ```python def application(environ, start_response): method = environ["REQUEST_METHOD"] path = environ.get("PATH_INFO", "/") if path == "/users" and method == "GET": body = json.dumps(user_list, ensure_ascii=False).encode("utf-8") start_response("200 OK", [ ("Content-Type", "application/json; charset=utf-8"), ("Content-Length", str(len(body))), ]) return [body] ``` たった1つのエンドポイントに対して、次のすべての手続きが必要です。 - メソッドの比較 - パスの比較 - JSON のシリアライズ - エンコーディング - `Content-Length` の計算 - `Content-Type` の指定 - `start_response` の呼び出し - イテラブルへのラップ エンドポイントが10個、20個と増えると、同じパターンのコードが果てしなく繰り返されます。 パスの比較は `if ... elif ... else` の長い連鎖になり、POST のボディを読むたびに `CONTENT_LENGTH` のチェックと `wsgi.input.read()` を書かなければなりません。 ```{warning} この冗長さは、ミスの温床でもあります。 - `Content-Length` の計算を間違える - エンコーディング前の文字列でバイト数を計算してしまう - `start_response` を呼び忘れる - イテラブルではなくバイト列を直接返してしまう これらの問題は、同じコードを何度も手で書くからこそ起きるものです。 ``` フレームワークは、この繰り返しとミスの可能性を排除するために存在します。 具体的に何を解決してくれるのかを、以下の4つの領域に分けて見ていきます。 | 領域 | 生の WSGI の問題 | フレームワークの解決策 | |------|------------------|------------------------| | request / response 抽象化 | `environ` 辞書の複雑なキー名 | `request.method` などの属性アクセス | | ルーティング | `if ... elif` の連鎖 | 宣言的なパターンマッチング | | ミドルウェア | `environ` を直接操作する煩雑さ | リクエスト/レスポンスオブジェクトを扱う形式 | | 例外ハンドリング | 各エンドポイントでの `try ... except` | システム全体の一元管理 | ## request / response 抽象化 生の WSGI では、リクエスト情報は `environ` 辞書にフラットに格納されています。 ```{mermaid} flowchart LR E["environ 辞書"] subgraph フレームワーク R["Request オブジェクト
request.method
request.path
request.body"] end subgraph 生WSGI K["environ['REQUEST_METHOD']
environ['PATH_INFO']
wsgi.input.read(n)"] end E --> K E --> R ``` メソッドは `environ["REQUEST_METHOD"]`、パスは `environ["PATH_INFO"]`、ヘッダーは `HTTP_` プレフィックス付きのキー、ボディは `environ["wsgi.input"]` のファイルライクオブジェクト——情報が存在する場所を知っていれば取り出せますが、直感的とは言いがたいインタフェースです。 フレームワークは、この `environ` 辞書を**リクエストオブジェクト**に変換します。 ```python # environ 辞書から直接取り出す(生の WSGI) method = environ["REQUEST_METHOD"] path = environ.get("PATH_INFO", "/") host = environ.get("HTTP_HOST", "") content_type = environ.get("CONTENT_TYPE", "") content_length = int(environ.get("CONTENT_LENGTH", "0") or "0") body = environ["wsgi.input"].read(content_length) # フレームワークのリクエストオブジェクト経由(Django の場合) method = request.method path = request.path host = request.headers["Host"] content_type = request.content_type body = request.body ``` ```{note} フレームワークのリクエストオブジェクトは、`environ` のキー名の変換規則を隠蔽し、`CONTENT_LENGTH` の空文字列チェックを引き受け、`wsgi.input` からの読み取りを管理してくれます。 開発者は `request.body` と書くだけです。 ``` レスポンス側も同様です。 生の WSGI では `start_response` を呼び、`Content-Length` を自分で計算し、バイト列のイテラブルを返す必要がありました。 フレームワークは**レスポンスオブジェクト**を提供し、これらの手続きをカプセル化します。 ```python # 生の WSGI body = json.dumps(data).encode("utf-8") start_response("200 OK", [ ("Content-Type", "application/json"), ("Content-Length", str(len(body))), ]) return [body] # Django return JsonResponse(data) # Flask return jsonify(data) ``` `JsonResponse` や `jsonify` が1行で済むのは、JSON のシリアライズ、エンコーディング、`Content-Type` の設定、`Content-Length` の計算をすべて内部で行っているからです。 そして最終的には、フレームワークの内部で `start_response` を呼び、イテラブルを返すという WSGI の手続きに変換されています。 ## ルーティング 4-6 の JSON API の例で、パスの分岐を `if ... elif ... else` で書きました。 パスが静的な文字列の比較だけであればなんとかなりますが、`/users/42` のような動的なパスパラメータが入ると途端に厄介になります。 ```python # 生の WSGI — 素朴なパスパラメータの処理 if path.startswith("/users/") and method == "GET": try: user_id = int(path.split("/")[2]) except (IndexError, ValueError): # エラー処理... ``` `path.split("/")[2]` でパスの3番目の要素を取り出すという力技です。 次のような場合はそれぞれ手動で対処するコードが必要です。 - `/users/42/posts` のようなネストしたパス - `/users/` のように末尾にスラッシュがある場合 - `/users/abc` のように数値でない値が入る場合 フレームワークのルーティングシステムは、この問題を宣言的に解決します。 ```python # Django path("users//", views.user_detail) # Flask @app.route("/users/") def user_detail(user_id): ... # FastAPI @app.get("/users/{user_id}") def user_detail(user_id: int): ... ``` ```{tip} パスのパターンを宣言し、パラメータの名前と型を指定するだけで、マッチング・パラメータの抽出・型変換が自動的に行われます。 パターンに一致しなければ 404 が返り、型変換に失敗すれば適切なエラーレスポンスが返ります。 ``` ルーティングシステムはさらに、次のような機能も提供します。 - URL の逆引き(名前から URL を生成する) - 正規表現によるパターンマッチ - 名前空間によるルートのグループ化 これらをすべて `if ... elif` で実装しようとすると、ルーティングだけでコードの大半を占めることになってしまいます。 ## ミドルウェア 1-3 で「玉ねぎの皮」モデルとして紹介したミドルウェアは、WSGI の上に構築されるフレームワークの重要な機能です。 WSGI 自体にもミドルウェアの概念はあります。 サーバから見ればアプリケーション、アプリケーションから見ればサーバとして振る舞う callable を間に挟むことができます。 しかし、生の WSGI ミドルウェアは `environ` と `start_response` を直接操作するため、書くのも読むのも煩雑です。 フレームワークは、ミドルウェアをより書きやすい形に抽象化しています。 Django であれば、リクエストオブジェクトとレスポンスオブジェクトを受け取るクラスとして書けます。 ```python # Django のミドルウェア class TimingMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): import time start = time.time() response = self.get_response(request) duration = time.time() - start response["X-Response-Time"] = f"{duration:.3f}s" return response ``` `request` はリクエストオブジェクト、`response` はレスポンスオブジェクトです。 `environ` の変換規則や `start_response` の呼び出しを意識する必要がありません。 認証の確認、CSRF トークンの検証、リクエストログの記録——こうした横断的な処理を、アプリケーションコードとは独立した場所に整理できます。 ## 例外ハンドリング 生の WSGI アプリケーションでは、例外処理はすべて自分で書く必要があります。 ```python def application(environ, start_response): try: # リクエスト処理... result = process(environ) body = json.dumps(result).encode("utf-8") start_response("200 OK", [ ("Content-Type", "application/json"), ("Content-Length", str(len(body))), ]) return [body] except KeyError: body = b'{"error": "Missing required field"}' start_response("400 Bad Request", [ ("Content-Type", "application/json"), ("Content-Length", str(len(body))), ]) return [body] except Exception: body = b'{"error": "Internal server error"}' start_response("500 Internal Server Error", [ ("Content-Type", "application/json"), ("Content-Length", str(len(body))), ]) return [body] ``` すべてのエンドポイントにこの `try ... except` を書くのは現実的ではありません。 また、例外の種類に応じて適切なステータスコードを返す判断を、毎回手動で行うのも間違いの元です。 フレームワークは、例外ハンドリングをシステム全体で一元化します。 - **Django**: 未処理の例外を自動的に 500 レスポンスに変換し、`Http404` 例外を 404 レスポンスに変換します - **FastAPI**: `HTTPException` をステータスコード付きの JSON レスポンスに変換し、Pydantic のバリデーションエラーを 422 レスポンスに変換します ```{important} 開発者は、ビジネスロジックで例外が発生したときに適切な例外クラスを投げるだけです。 その例外を HTTP レスポンスに変換する作業は、フレームワークが引き受けてくれます。 ``` --- 本節では、生の WSGI がつらい理由を request / response の抽象化、ルーティング、ミドルウェア、例外ハンドリングという4つの領域に分けて整理しました。 これらはすべて、WSGI の仕様自体には含まれていない「WSGI の上に必要なもの」です。 次節からは、これらの「必要なもの」を実際にどう提供しているかを、Werkzeug と Bottle という2つのライブラリの内部を見ながら確認していきます。 どちらも WSGI の `environ` と `start_response` の上に薄い抽象層を重ねたものであり、フレームワークの本質を理解するのに最適な教材です。 (Werkzeug を理解する)= ## Werkzeug を理解する 前節で、生の WSGI の上に何が必要かを整理しました。 本節では、その「必要なもの」を提供しているライブラリのひとつである **Werkzeug** の内部を見ていきます。 Werkzeug は Flask の基盤として広く知られていますが、Flask とは独立したライブラリです。 Werkzeug を理解することは、Flask を理解することであり、さらに言えば「WSGI フレームワークが最低限何をしているのか」を理解することでもあります。 ### Werkzeug の立ち位置 Werkzeug(ドイツ語で「工具」を意味する)は、自らを「WSGI ユーティリティライブラリ」と位置づけています。 フレームワークそのものではなく、フレームワークを作るための部品集です。 この位置づけを理解するために、WSGI エコシステムの層構造を整理しましょう。 ``` WSGI サーバ(Gunicorn, uWSGI) ↓ environ / start_response WSGI ユーティリティ(Werkzeug) ↓ Request / Response オブジェクト, ルーティング フレームワーク(Flask) ↓ デコレータ, テンプレート, 設定管理 アプリケーションコード(あなたのコード) ``` ```{mermaid} flowchart TD S["WSGIサーバ
Gunicorn / uWSGI"] W["Werkzeug
Request/Response/ルーティング"] F["Flask
デコレータ/テンプレート"] A["アプリケーションコード"] S -->|"environ / start_response"| W W -->|"Request / Response オブジェクト"| F F -->|"ビュー関数"| A ``` WSGI サーバとアプリケーションコードの間に、Werkzeug と Flask が重なっています。 - **Werkzeug**: WSGI の `environ` と `start_response` を扱いやすいオブジェクトに変換する層 - **Flask**: その上にルーティングのデコレータ構文やテンプレートエンジンの統合などの「開発者体験」を追加する層 Werkzeug が提供する主要な機能は次の通りです。 | 機能 | 役割 | |------|------| | `Request` / `Response` オブジェクト | `environ` 辞書を扱いやすい形に変換 | | URL ルーティング | パスのパターンマッチングと型変換 | | デバッガ | ブラウザ上でのインタラクティブなスタックトレース | | 開発用サーバ | 手軽なローカル開発環境 | ```{tip} Werkzeug のコードを読むと、`environ` 辞書をどう加工しているか、`start_response` をどうラップしているかが見えるため、「WSGI の上にフレームワークを作るとはどういうことか」を具体的に理解できます。 ``` ### Request / Response オブジェクト Werkzeug の最も基本的な貢献は、`environ` 辞書を `Request` オブジェクトに、レスポンスの組み立てを `Response` オブジェクトに変換することです。 まず、`Request` オブジェクトを見てみましょう。 ```python from werkzeug.wrappers import Request def application(environ, start_response): request = Request(environ) # environ から直接取り出す場合 # method = environ["REQUEST_METHOD"] # path = environ.get("PATH_INFO", "/") # host = environ.get("HTTP_HOST", "") # content_length = int(environ.get("CONTENT_LENGTH", "0") or "0") # body = environ["wsgi.input"].read(content_length) # Request オブジェクト経由 method = request.method # "GET" path = request.path # "/users/42" host = request.host # "127.0.0.1:8000" body = request.get_data() # b'{"name": "Taro"}' json_data = request.get_json() # {"name": "Taro"} # ... ``` `Request(environ)` は、`environ` 辞書をラップして属性アクセスできるようにしたオブジェクトです。それぞれの内部動作は次のようになっています。 - `request.method`: 内部で `environ["REQUEST_METHOD"]` を返す - `request.host`: `environ.get("HTTP_HOST")` を返し、存在しなければ `SERVER_NAME` と `SERVER_PORT` から組み立てる - `request.get_json()`: `CONTENT_TYPE` の確認 → `CONTENT_LENGTH` の読み取り → `wsgi.input` からのボディ読み取り → JSON パース → Python 辞書として返す 前章で自分の手で書いた面倒な処理が、メソッド呼び出しひとつに凝縮されています。 `Response` オブジェクトも同様に、レスポンスの組み立てを簡潔にします。 ```python from werkzeug.wrappers import Request, Response def application(environ, start_response): request = Request(environ) if request.path == "/" and request.method == "GET": response = Response("Welcome!", content_type="text/plain") elif request.path == "/json" and request.method == "GET": response = Response( '{"message": "hello"}', content_type="application/json", ) else: response = Response("Not Found", status=404) return response(environ, start_response) ``` `Response` オブジェクトを作成する際に、ボディ、ステータスコード、`Content-Type` を指定します。 `Content-Length` の計算は `Response` が内部で行い、エンコーディングも自動です。 ```{note} 注目すべきは最後の行 `return response(environ, start_response)` です。Werkzeug の `Response` オブジェクトは、それ自体が **WSGI アプリケーション callable** として動作します。 `response(environ, start_response)` を呼ぶと、内部で `start_response` を適切に呼び出し、ボディのイテラブルを返します。 `Response` クラスの `__call__` メソッドが、WSGI の手続きをすべて引き受けてくれるのです。 ``` この設計は巧みです。 Werkzeug の `Response` を使っても、アプリケーション全体は依然として WSGI に準拠した callable です。 Gunicorn からも uWSGI からも、通常の WSGI アプリケーションとして呼び出せます。 WSGI の互換性を保ちながら、開発者の体験を向上させているのが「WSGI ユーティリティ」の立ち位置です。 ### URL routing Werkzeug は、URL ルーティングの仕組みも提供しています。 `Map` と `Rule` を使って、パスのパターンとエンドポイント名の対応を定義します。 ```python from werkzeug.routing import Map, Rule from werkzeug.wrappers import Request, Response url_map = Map([ Rule("/", endpoint="index"), Rule("/users", endpoint="user_list"), Rule("/users/", endpoint="user_detail"), ]) def on_index(request): return Response("Welcome!") def on_user_list(request): return Response("User list") def on_user_detail(request, user_id): return Response(f"User {user_id}") views = { "index": on_index, "user_list": on_user_list, "user_detail": on_user_detail, } def application(environ, start_response): request = Request(environ) adapter = url_map.bind_to_environ(environ) try: endpoint, values = adapter.match() response = views[endpoint](request, **values) except NotFound: response = Response("Not Found", status=404) except MethodNotAllowed: response = Response("Method Not Allowed", status=405) return response(environ, start_response) ``` `Rule("/users/", endpoint="user_detail")` という記述で、`/users/42` のようなパスにマッチし、`42` を整数として `user_id` に取り出すルールが定義されます。 `adapter.match()` がマッチングを実行し、エンドポイント名と抽出されたパラメータを返します。 {numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)で `path.startswith("/users/")` と `int(path.split("/")[2])` を組み合わせて苦労していたパスパラメータの処理が、`` という宣言的な記法で解決されています。 パスに一致しなければ `NotFound` 例外が、メソッドが許可されていなければ `MethodNotAllowed` 例外が発生するため、404 と 405 の区別も自然に行えます。 ```{admonition} コラム: Flask のルーティングと Werkzeug の関係 :class: tip Flask の `@app.route("/users/")` デコレータは、この Werkzeug のルーティングシステムの上に構文糖を載せたものです。 Flask は `@app.route()` が呼ばれるたびに内部で `Rule` オブジェクトを作成し、`Map` に追加しています。 デコレータの裏側で行われているのは、ここで見たのと本質的に同じ処理です。 なお、Django のルーティングシステムは Werkzeug とは独立に開発されたもので、内部構造は異なります。 しかし、「パスのパターンを定義し、リクエストのパスと照合し、パラメータを抽出する」という責務は同じです。 ``` ### debugger, reloader への軽い言及 Werkzeug は、Request / Response とルーティングの他にも、開発を支援する2つの重要な機能を提供しています。 デバッガとリローダです。 Werkzeug のデバッガは、アプリケーションで未処理の例外が発生したとき、ブラウザ上にインタラクティブなスタックトレースを表示します。 各フレームの変数を確認でき、ブラウザ上で Python のコードを実行することすらできます。 Flask の開発サーバで見かけるあの詳細なエラーページは、Werkzeug のデバッガが生成しているものです。 この機能は WSGI ミドルウェアとして実装されています。 アプリケーションを Werkzeug のデバッガミドルウェアでラップすると、アプリケーション内で例外が発生した場合にミドルウェアがそれをキャッチし、スタックトレースを HTML としてレスポンスに含めます。 WSGI のミドルウェアの仕組み——アプリケーションの前後に処理を挟む——を活用した実装です。 リローダは、ソースコードの変更を監視し、変更が検出されたらサーバプロセスを自動的に再起動する機能です。 Flask の `app.run(debug=True)` で有効になるコード変更時の自動リロードは、Werkzeug のリローダが担当しています。 ```{danger} デバッガとリローダは、**本番環境では絶対に使ってはいけません**。 - **デバッガ**: ブラウザ上で任意の Python コードを実行できるため、本番で有効にすると深刻なセキュリティ上の脆弱性になります - **リローダ**: パフォーマンスのオーバーヘッドがあり、本番環境には不適切です `DEBUG = True` や `debug=True` を本番環境で設定しないことは、Web 開発の基本的なセキュリティプラクティスです。 ``` --- 本節では、Werkzeug が WSGI の上にどのような抽象層を提供しているかを確認しました。 - `environ` 辞書を `Request` オブジェクトに変換する - レスポンスの組み立てを `Response` オブジェクトに委ねる - `Map` と `Rule` でルーティングを宣言的に記述する - デバッガとリローダで開発体験を向上させる これらはすべて、WSGI の `environ` と `start_response` の上に構築されています。 次節では、Werkzeug とは異なるアプローチで WSGI の上に抽象層を構築した **Bottle** の内部を覗きます。 Bottle はフレームワーク全体が1つのファイルに収まるという特徴を持ち、フレームワークの全体像を一望できる貴重な教材です。 (Bottle を理解する)= ## Bottle を理解する 前節では Werkzeug が WSGI の上にユーティリティ層を構築するアプローチを見ました。 本節では、まったく異なる設計哲学を持つ **Bottle** を取り上げます。 Bottle は「フレームワーク全体が1つの Python ファイルに収まる」という特徴を持つマイクロフレームワークです。 この極端なコンパクトさが、フレームワークの内部構造を学ぶうえで大きな利点になります。 Werkzeug が部品集として個々の機能を提供するのに対し、Bottle はルーティング、リクエスト処理、レスポンス生成、テンプレートエンジンまでをひとつのファイルの中で完結させています。 ### 単一ファイル志向の軽さ Bottle のソースコードは `bottle.py` という単一のファイルです。 外部ライブラリへの依存はゼロで、Python の標準ライブラリだけで動作します。 ```bash pip install bottle ``` インストールすると、`bottle.py` が1ファイルだけ配置されます。 このファイルの中に、フレームワークのすべてが入っています。 | 機能 | 説明 | |------|------| | ルーティングシステム | パスパターンのマッチングと型変換 | | リクエストオブジェクト | `environ` 辞書のラッパー | | レスポンスオブジェクト | ステータス・ヘッダー・ボディの管理 | | テンプレートエンジン | 簡易的な HTML テンプレート | | 開発用サーバ | WSGI アダプタとサーバ起動機能 | ```{admonition} コラム: 単一ファイルで学ぶフレームワーク設計 :class: tip Django のソースコードは数百のファイル、数万行に及びます。 Flask も Werkzeug を含めると相当な規模になります。 しかし Bottle であれば、ファイルを上から下まで読むだけで、フレームワークが何をしているかの全体像が見えます。 この設計哲学は、Bottle の作者 Marcel Hellkamp が「フレームワークの全体像を1ファイルで把握できること」を重視した結果です。 ``` Bottle で最小のアプリケーションを書いてみましょう。 ```python # app.py from bottle import route, run @route("/") def index(): return "Hello, Bottle!" @route("/greet/") def greet(name): return f"Hello, {name}!" run(host="127.0.0.1", port=8000) ``` ```bash python app.py ``` これだけで、ルーティング付きの Web アプリケーションが動きます。 `curl http://127.0.0.1:8000/greet/Taro` にアクセスすれば `Hello, Taro!` が返ってきます。 このコードの裏側では次のことが行われています。 1. `@route("/")` デコレータが、パスのパターンとビュー関数の対応を Bottle の内部ルーティングテーブルに登録する 2. `run()` が開発用の WSGI サーバを起動する 3. リクエストが届くたびに Bottle の WSGI アプリケーションが呼び出される 4. `environ` を受け取り、ルーティングテーブルを検索し、一致したビュー関数を呼び出し、戻り値をレスポンスに変換して返す ### デコレータベースのルーティング Bottle のルーティングは、デコレータ構文で宣言的に定義します。 このスタイルは後に Flask にも採用され、Python Web フレームワークの定番の書き方になりました。 ```{mermaid} flowchart TD D["@get('/users/\')"] T[ルーティングテーブルに登録] R[リクエスト受信] M{パス/メソッド
マッチング} H["user_detail(user_id=42) 呼び出し"] N[404 Not Found] D --> T R --> M M -->|一致| H M -->|不一致| N ``` ```python from bottle import get, post, request @get("/users") def user_list(): return {"users": ["Taro", "Hanako"]} @get("/users/") def user_detail(user_id): return {"id": user_id, "name": "Taro"} @post("/users") def create_user(): data = request.json return {"created": data} ``` ```{note} `@get` と `@post` は、それぞれ GET リクエストと POST リクエストにのみ反応するデコレータです。 `@route` デコレータに `method` 引数を渡す形式もありますが、`@get`/`@post` のほうが意図が明確です。 `` はパスパラメータの宣言で、`:int` はフィルタと呼ばれる型変換の指定です。 Werkzeug の `` と記法は異なりますが、やっていることは同じ——パスの該当部分を整数に変換し、ビュー関数の引数として渡します。 変換に失敗した場合(たとえば `/users/abc`)は 404 が返ります。 ``` このデコレータの内部で何が起きているかを、概念的に示すと次のようになります。 ```python # @get("/users/") の疑似的な内部動作 def get(path): def decorator(func): # ルーティングテーブルにエントリを追加 app.routes.append({ "method": "GET", "pattern": compile_pattern(path), # パスをパターンに変換 "callback": func, }) return func return decorator ``` デコレータは関数を受け取り、その関数をルーティングテーブルに登録して、元の関数をそのまま返します。 リクエストが届いたとき、Bottle はルーティングテーブルを走査し、パスとメソッドが一致するエントリを見つけ、対応する `callback` を呼び出します。 {numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)で `if path == "/users" and method == "GET":` と書いていた分岐が、デコレータの裏側で自動的に構築されるルーティングテーブルに置き換わっているのです。 Bottle のビュー関数の戻り値が柔軟なのも特徴的です。 | 戻り値の型 | レスポンスの形式 | |-----------|----------------| | 文字列 | `text/html` | | 辞書 | `application/json`(自動でシリアライズ) | | `Response` オブジェクト | そのまま使用 | ```text # 文字列 → text/html レスポンス @get("/html") def html(): return "

Hello

" # 辞書 → application/json レスポンス @get("/json") def json_example(): return {"message": "hello"} ``` この「戻り値の型を見てレスポンスの形式を自動判定する」仕組みは、ビュー関数の中で `start_response` の呼び出しや `Content-Type` の設定を意識しなくて済むようにするためです。 Bottle の内部では、ビュー関数の戻り値を検査し、文字列であればエンコードしてボディにし、辞書であれば `json.dumps()` でシリアライズし、最終的に WSGI のイテラブルに変換しています。 ### WSGI アプリとしての Bottle Bottle の最も重要な性質は、**Bottle アプリケーション自体が WSGI アプリケーションである**という点です。 ```python from bottle import Bottle app = Bottle() @app.get("/") def index(): return "Hello!" ``` この `app` は WSGI の callable です。`app(environ, start_response)` と呼び出すことができます。つまり、Gunicorn の上でそのまま動かせます。 ```bash gunicorn app:app ``` Bottle のソースコードの中で、この WSGI callable としての動作を担うのが `Bottle.__call__` メソッドです。概念的に単純化すると、次のような処理が行われています。 ```python # Bottle.__call__ の概念的な流れ(大幅に簡略化) class Bottle: def __call__(self, environ, start_response): # 1. environ を Request オブジェクトに変換 request = Request(environ) # 2. ルーティング — パスとメソッドに一致するハンドラを探す handler, params = self.router.match(request.path, request.method) # 3. ハンドラを実行 try: result = handler(**params) except HTTPError as e: result = e # 4. 戻り値を Response に変換 if isinstance(result, dict): response = Response(json.dumps(result), content_type="application/json") elif isinstance(result, str): response = Response(result, content_type="text/html") else: response = result # 5. WSGI の形式で返す return response(environ, start_response) ``` このフローは、{numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)で自分の手で書いた WSGI アプリケーションの構造と本質的に同じです。 1. `environ` を受け取る 2. パスとメソッドでルーティングする 3. ハンドラーを実行する 4. 結果をレスポンスに変換する 5. WSGI の形式で返す Bottle はこの一連の処理を、デコレータと型の自動判定で開発者にとって書きやすくしているのです。 Bottle が WSGI アプリケーションであるということは、WSGI ミドルウェアをそのまま適用できることも意味します。 ```python from bottle import Bottle app = Bottle() @app.get("/") def index(): return "Hello!" # WSGI ミドルウェアを適用 from some_middleware import SomeMiddleware wrapped_app = SomeMiddleware(app) ``` ```{important} `SomeMiddleware` は Bottle の存在を知りません。WSGI の callable を受け取って、WSGI の callable を返すだけです。 Werkzeug のミドルウェアも、Django 用の WSGI ミドルウェアも、Bottle に対して適用できます。これが WSGI の相互運用性の力です。 ``` --- 本節では、Bottle の内部を3つの観点から見てきました。 - 単一ファイルに凝縮されたフレームワークの全体像 - デコレータによる宣言的なルーティングとその内部動作 - WSGI アプリケーションとしての本質 Werkzeug がユーティリティとして部品を提供し、Flask がそれを組み合わせてフレームワークにするのに対し、Bottle はすべてを1ファイルの中で完結させます。 アプローチは異なりますが、どちらも WSGI の `environ` と `start_response` の上に構築されているという点は共通しています。 そして、どちらのフレームワークで書いたアプリケーションも、WSGI に準拠したサーバであればどこでも動きます。 次節では、Flask が Werkzeug の部品をどのように組み合わせてフレームワークとしての体験を提供しているかを確認し、そこから WSGI ミドルウェアの仕組みへと進みます。 (WSGI ミドルウェアを書く)= ## WSGI ミドルウェアを書く 前節まで、Werkzeug と Bottle が WSGI の上にどのような抽象層を構築しているかを見てきました。 本節では、WSGI のもうひとつの重要な概念——**ミドルウェア**——を、自分の手で書きながら理解します。 1-3 で「玉ねぎの皮」モデルとして紹介したミドルウェアの仕組みが、WSGI ではどう実現されているのか。 その構造は驚くほどシンプルです。 ### callable を包む構造 WSGI ミドルウェアの本質は、**ある WSGI アプリケーションを別の WSGI アプリケーションで包む**ことです。 WSGI アプリケーションは `application(environ, start_response)` という形式の callable です。 ミドルウェアは、この callable を受け取り、自身も同じ形式の callable として振る舞います。 サーバから見ればミドルウェアはアプリケーションであり、アプリケーションから見ればミドルウェアはサーバです。 構造を疑似コードで表すと、こうなります。 ```python class Middleware: def __init__(self, app): self.app = app # 元のアプリケーションを保持 def __call__(self, environ, start_response): # リクエスト処理の前に何かする ... # 元のアプリケーションを呼び出す response = self.app(environ, start_response) # レスポンス処理の後に何かする ... return response ``` `__init__` で元のアプリケーションを受け取って保持し、`__call__` で WSGI の callable として振る舞います。 自分の処理を挟みつつ、最終的には元のアプリケーションに処理を委譲する——これがミドルウェアのすべてです。 ミドルウェアを適用するのも単純です。 ```python # 元のアプリケーション def application(environ, start_response): start_response("200 OK", [("Content-Type", "text/plain")]) return [b"Hello, World!"] # ミドルウェアで包む wrapped = Middleware(application) # サーバから見ると wrapped が「アプリケーション」 # wrapped(environ, start_response) で呼び出される ``` 複数のミドルウェアを重ねることもできます。 ```python app = application app = MiddlewareA(app) app = MiddlewareB(app) app = MiddlewareC(app) ``` リクエストとレスポンスは次の順序で流れます。 - **リクエスト(外側 → 内側)**: MiddlewareC → MiddlewareB → MiddlewareA → application - **レスポンス(内側 → 外側)**: application → MiddlewareA → MiddlewareB → MiddlewareC まさに玉ねぎの皮です。 ```{mermaid} flowchart LR subgraph MiddlewareC subgraph MiddlewareB subgraph MiddlewareA APP[application] end end end REQ[リクエスト] -->|外 → 内| MiddlewareC MiddlewareC -->|内 → 外| RES[レスポンス] ``` ```{note} この仕組みが成立するのは、WSGI がインタフェースを統一しているからです。 すべてのミドルウェアとすべてのアプリケーションが `(environ, start_response) → iterable` という同じ形式に従っているため、任意の順序で自由に組み合わせられます。 ``` ### ロギングミドルウェア 具体的なミドルウェアを書いてみましょう。 最初は、すべてのリクエストのメソッド、パス、ステータスコード、処理時間をログに出力するミドルウェアです。 ```text # logging_middleware.py import sys import time class LoggingMiddleware: def __init__(self, app): self.app = app def __call__(self, environ, start_response): method = environ["REQUEST_METHOD"] path = environ.get("PATH_INFO", "/") start_time = time.time() # ステータスコードをキャプチャするためにstart_responseをラップ captured_status = [None] def custom_start_response(status, headers, exc_info=None): captured_status[0] = status return start_response(status, headers, exc_info) # 元のアプリケーションを呼び出す result = self.app(environ, custom_start_response) elapsed = (time.time() - start_time) * 1000 status = captured_status[0] or "unknown" print( f'{method} {path} → {status} ({elapsed:.1f}ms)', file=sys.stderr, ) return result ``` ```{tip} このミドルウェアで注目してほしいのは、`start_response` をラップしている部分です。 ステータスコードはアプリケーションが `start_response` を呼ぶときに渡されますが、ミドルウェアから見ると、その呼び出しはアプリケーション内部で行われるため直接観測できません。 そこで、元の `start_response` の代わりに `custom_start_response` を渡し、アプリケーションが呼んだ時点でステータスコードを記録しています。 `captured_status` がリスト(`[None]`)になっているのは、Python のクロージャの仕組みに関連しています。 内側の関数から外側の変数を再代入するには、ミュータブルなオブジェクト(リスト)を使うか、`nonlocal` 宣言を使う必要があります。 ``` 使い方は簡単です。 ```python from wsgiref.simple_server import make_server from logging_middleware import LoggingMiddleware def application(environ, start_response): path = environ.get("PATH_INFO", "/") if path == "/": start_response("200 OK", [("Content-Type", "text/plain")]) return [b"Hello!"] else: start_response("404 Not Found", [("Content-Type", "text/plain")]) return [b"Not Found"] app = LoggingMiddleware(application) server = make_server("127.0.0.1", 8000, app) server.serve_forever() ``` リクエストを送るたびに、標準エラー出力にログが表示されます。 ``` GET / → 200 OK (0.3ms) GET /about → 404 Not Found (0.1ms) POST /users → 200 OK (12.5ms) ``` 3-7 で「ログに何を出すべきか」を議論しましたが、このミドルウェアはまさにあの議論を実装したものです。 そして、このミドルウェアは特定のフレームワークに依存していません。 Django でも Flask でも Bottle でも、WSGI アプリケーションであれば何にでも適用できます。 ### ヘッダー追加ミドルウェア 次に、すべてのレスポンスにカスタムヘッダーを追加するミドルウェアを書いてみましょう。 セキュリティ関連のヘッダーを一括で付与するのは、実務でもよくある用途です。 ```python # security_headers_middleware.py class SecurityHeadersMiddleware: def __init__(self, app): self.app = app def __call__(self, environ, start_response): def custom_start_response(status, headers, exc_info=None): # セキュリティ関連のヘッダーを追加 headers.append(("X-Content-Type-Options", "nosniff")) headers.append(("X-Frame-Options", "DENY")) headers.append(("X-XSS-Protection", "1; mode=block")) return start_response(status, headers, exc_info) return self.app(environ, custom_start_response) ``` ロギングミドルウェアと同じく `start_response` をラップしていますが、今度はステータスコードを記録するのではなく、ヘッダーのリストに要素を追加してから元の `start_response` に渡しています。 アプリケーションが `start_response("200 OK", [("Content-Type", "text/html")])` を呼ぶと、`custom_start_response` の中でセキュリティヘッダーが3つ追加され、最終的に `start_response` に渡されるヘッダーは4つになります。 アプリケーション側はこのミドルウェアの存在を知りません。 すべてのレスポンスに自動的にセキュリティヘッダーが付与されます。 `curl -v` で確認すると、追加されたヘッダーが見えます。 ``` < HTTP/1.0 200 OK < Content-Type: text/html < X-Content-Type-Options: nosniff < X-Frame-Options: DENY < X-XSS-Protection: 1; mode=block ``` ```{note} Django の `SecurityMiddleware` も、同様の仕組みでセキュリティヘッダーを追加しています。 Django のミドルウェアは WSGI ミドルウェアとは形式が異なりますが(Django 独自のリクエスト/レスポンスオブジェクトを使う)、「レスポンスが返される前にヘッダーを追加する」という責務は同じです。 ``` ### 例外処理ミドルウェア 最後に、アプリケーション内で発生した未処理の例外をキャッチし、500 エラーレスポンスを返すミドルウェアを書きます。 ```python # error_handling_middleware.py import sys import traceback class ErrorHandlingMiddleware: def __init__(self, app, debug=False): self.app = app self.debug = debug def __call__(self, environ, start_response): try: return self.app(environ, start_response) except Exception: # スタックトレースを標準エラー出力に記録 traceback.print_exc(file=sys.stderr) if self.debug: # デバッグモード:スタックトレースをレスポンスに含める tb = traceback.format_exc() body = f"Internal Server Error\n\n{tb}".encode("utf-8") else: # 本番モード:詳細は隠す body = b"Internal Server Error" start_response("500 Internal Server Error", [ ("Content-Type", "text/plain; charset=utf-8"), ("Content-Length", str(len(body))), ]) return [body] ``` このミドルウェアは、`self.app(environ, start_response)` の呼び出しを `try ... except` で囲んでいます。 アプリケーション内のどこかで例外が発生すると、ここでキャッチされます。`debug=True` であればスタックトレースをレスポンスに含め、`False` であれば汎用的なエラーメッセージだけを返します。 ```python def application(environ, start_response): # わざと例外を発生させる raise ValueError("Something went wrong!") app = ErrorHandlingMiddleware(application, debug=True) ``` このアプリケーションにリクエストを送ると、ブラウザには500エラーとスタックトレースが表示されます。 ミドルウェアがなければ、サーバプロセスがクラッシュするか、サーバ固有のエラーページが表示されるかのどちらかです。 ```{caution} ここで注意すべき点がひとつあります。 アプリケーションが `start_response` を呼んだ後、イテラブルの走査中に例外が発生した場合、ステータスとヘッダーはすでにクライアントに送信されている可能性があります。 HTTP の性質上、一度送信したステータスラインは取り消せません。 この場合、ミドルウェアは `start_response` に `exc_info` を渡して再呼び出しを試みますが、ボディの送信が始まっていれば手遅れです。 4-4 で説明した `start_response` の `exc_info` 引数の制約が、ここで実際の問題として現れます。 ``` Werkzeug のデバッガミドルウェアは、この例外処理ミドルウェアの高機能版です。 スタックトレースを美しい HTML で表示し、ブラウザ上で変数の中身を確認でき、インタラクティブに Python コードを実行できます。 しかし根底にある仕組みは同じで、アプリケーションの呼び出しを `try ... except` で囲み、例外をキャッチしてエラーレスポンスに変換しているのです。 --- 本節では、WSGI ミドルウェアを3つ自作しました。 ロギング、ヘッダー追加、例外処理はいずれも、フレームワークが標準で提供している機能の原始的な姿です。 WSGI ミドルウェアの構造は極めてシンプルです。 元のアプリケーションを保持し、自身も WSGI callable として振る舞い、前後に処理を挟みつつ元のアプリケーションに委譲します。 このシンプルさは WSGI のインタフェースが統一されているからこそ実現できるものであり、WSGI の設計がいかに優れていたかを改めて示しています。 次節では、これまでに学んだ Werkzeug、Bottle、WSGI ミドルウェアの知識を踏まえて、Flask がそれらをどう組み合わせてフレームワークとしての体験を提供しているかを確認します。 (Flask の位置づけを整理する)= ## Flask の位置づけを整理する ### Flask は Werkzeug の上にある Flask の中核は Werkzeug が提供する部品の組み合わせです。 Flask アプリケーションを作成すると、内部では Werkzeug の `Request`、`Response`、`Map`/`Rule`(URL ルーティング)、開発用サーバ、デバッガ、リローダがそのまま使われています。 ```python from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/users/") def user_detail(user_id): return jsonify({"id": user_id, "name": "Taro"}) ``` この短いコードの裏で起きていることを分解すると次のようになります。 1. `Flask.__init__` で Werkzeug の `Map` が生成される 2. `@app.route` デコレータが `Rule` を追加する 3. リクエストが届くと `Flask.__call__(environ, start_response)` が呼ばれる 4. 内部で Werkzeug の `Request(environ)` を生成し、`Map.bind_to_environ` でルートをマッチングし、対応するビュー関数を呼び出す 5. ビューの戻り値は Werkzeug の `Response` オブジェクトに変換され、最終的に `response(environ, start_response)` で WSGI レスポンスとして返却される ```{important} Flask は「Werkzeug を直接使う煩雑さを、デコレータとコンテキスト管理で包んだ薄い層」であり、独自の HTTP パーサやルーティングエンジンを持っているわけではありません。 ``` ### 「ミニフレームワーク」とは何か Flask は自身を「マイクロフレームワーク」と称しています。これは機能が少ないという意味ではなく、**コアを最小限に保ち、必要な機能を拡張(Extension)で追加する設計思想** を指しています。 | フレームワーク | アプローチ | 主な同梱機能 | |--------------|-----------|------------| | Django | batteries-included | ORM、管理画面、認証、フォーム、テンプレート | | Flask | マイクロ(拡張で補う) | ルーティング、リクエスト/レスポンス抽象化、Jinja2、セッション | | Bottle | 単一ファイル完結 | 上記すべてを1ファイルで実装 | データベース連携が必要なら Flask-SQLAlchemy、認証なら Flask-Login、API 構築なら Flask-RESTful というように、プロジェクトの要件に応じて組み合わせます。 この設計は前節の Bottle と似ていますが、Flask は Werkzeug という堅牢な WSGI ツールキットの上に構築されている点が異なります。 Bottle が単一ファイルで全てを自前実装するのに対し、Flask は Werkzeug に HTTP 処理を委譲し、自身はアプリケーション構造(Blueprint、コンテキスト、Extension 機構)に集中しています。 ### 本書で Flask を主題にしない理由 本書が Flask ではなく Django と FastAPI を主軸に据える理由は三つあります。 **理由 1: Werkzeug の解説で Flask の中核はカバー済み** Flask の内部を理解するとは、実質的に Werkzeug の内部を理解することです。 前節で Werkzeug の Request/Response、ルーティング、デバッガを解説した時点で、Flask の中核メカニズムはすでに説明済みです。 Flask 固有の仕組み(アプリケーションコンテキスト、リクエストコンテキスト、Blueprint)は重要ですが、本書のテーマである「HTTP リクエストがアプリに届き処理されてレスポンスになる流れ」に対する追加情報は限定的です。 **理由 2: WSGI と ASGI の両方を扱うため** 本書は WSGI と ASGI の両方を扱います。 Flask は WSGI フレームワークであり、ASGI への対応は限定的です。 Django は WSGI と ASGI の両方をサポートし(Vol.2「Django を WSGI 視点で見る」・Vol.2「Django は ASGI にどう対応しているか」)、FastAPI は ASGI ネイティブ(Vol.2「FastAPI を ASGI 視点で見る」)であるため、両者を並べることで WSGI から ASGI への進化を一貫して追跡できます。 **理由 3: 対照的なアプローチの比較** Django と FastAPI は「batteries-included で大規模向け」と「型ヒント活用で API 特化」という対照的なアプローチを取っており、比較を通じてフレームワーク設計の選択肢を幅広く提示できます。 ```{tip} Flask に精通している読者は、{numref}`WSGI の上に何が必要になるのか`({ref}`WSGI の上に何が必要になるのか`)までの知識を持って Flask のソースコードを読めば、`flask/app.py` の `wsgi_app` メソッドが Werkzeug のルーティングとリクエスト/レスポンスをどのように統合しているかを自力で追跡できるはずです。 ``` --- 次節では、WSGI の上に重ねてきた抽象化の層が開発にもたらす利益と、同時に何を見えなくするのかを整理します。 (抽象化がもたらす利益と見えなくなるもの)= ## 抽象化がもたらす利益と見えなくなるもの {numref}`HTTP は何をやりとりしているのか`({ref}`HTTP は何をやりとりしているのか`)で TCP ソケットから生バイト列を読み取り、{numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で自前の HTTP サーバを書き、{numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)で WSGI によるサーバとアプリの分離を学び、{numref}`WSGI の上に何が必要になるのか`({ref}`WSGI の上に何が必要になるのか`)で Werkzeug・Bottle・Flask がその上にリクエスト/レスポンス抽象化やルーティングを重ねる様子を見てきました。 抽象化の層が増えるほど開発は楽になりますが、同時に「何が起きているか」が見えなくなります。 本節ではその利益と代償を整理します。 ### 開発速度 抽象化の最大の恩恵は、ビジネスロジックへの集中です。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で自作した HTTP サーバでは、ユーザー一覧を JSON で返すだけでも次のすべての処理を毎回書く必要がありました。 - ソケット生成 - ヘッダー終端検出ループ - `Content-Length` 計算 - ステータスライン組み立て - バイト列エンコーディング Flask であれば同じことが数行で済みます。 ```python @app.route("/users") def user_list(): users = get_users_from_db() return jsonify(users) ``` Django であれば `JsonResponse(data)` の一行で、ステータスコード設定、`Content-Type` ヘッダー付与、JSON シリアライズ、`Content-Length` 計算、バイト列変換のすべてが完了します。 ```{important} 開発者が書くコードの量が減るだけでなく、定型処理のバグ(エンコーディングミス、ヘッダー漏れ、接続クローズ忘れなど)も構造的に排除されます。 ``` この効率化は個人開発でもチーム開発でも効果を発揮します。 新しいエンドポイントの追加が数分で終わるため、プロトタイピングの速度が上がり、仕様変更への対応コストも下がります。 LLM を使ったコード生成においても、フレームワークの規約に沿ったコードは生成精度が高く、生のソケット操作を含むコードよりも正確に出力される傾向があります。 ### 認知負荷の削減 抽象化は「考えなくてよいこと」を増やします。 Flask のビュー関数を書いているとき、開発者は TCP の 3 ウェイハンドシェイクや `recv()` のバッファサイズを意識する必要がありません。 Django の ORM を使っているとき、SQL のエスケープ処理やコネクションプーリングの詳細を知らなくてもアプリケーションは動きます。 これは単に楽をしているのではなく、人間の作業記憶には限界があるという現実に対応した設計です。 一度に7つ前後の要素しか扱えないとされる認知の制約の中で、次のすべてを同時に考えることは不可能です。 - TCP 接続管理 - HTTP パース - WSGI プロトコル - ルーティング - 認証 - ビジネスロジック - レスポンス生成 抽象化は下位層を「信頼できるブラックボックス」に変え、開発者が当面の問題に集中できる環境を作ります。 フレームワークの規約 (Django の `urlpatterns`、Flask の `@app.route`、FastAPI の型ヒント) も認知負荷の削減に寄与しています。パターンが統一されていれば、他人が書いたコードでもエンドポイントの場所、リクエストの受け取り方、レスポンスの返し方が予測でき、コードリーディングの速度が上がります。 ### 低レイヤ理解の不在による落とし穴 しかし抽象化は、問題が起きたときに牙を剥きます。 正常系では見えなくて構わなかった下位層が、異常系では突然顔を出すからです。 ```{warning} **落とし穴 1: 502 Bad Gateway の原因調査** 本番環境で `502 Bad Gateway` が発生したとき、Django のコードだけを見ても原因は分かりません。 502 は Nginx がアップストリーム(Gunicorn)から正常なレスポンスを受け取れなかったことを意味しますが、その原因は次のようなものが考えられます。 - Gunicorn のワーカータイムアウト - アプリケーション内の無限ループ - メモリ不足によるワーカークラッシュ - Unix ソケットのパーミッションエラー これらはいずれも、フレームワークが隠してくれていた層で起きている問題です。 ``` ```{warning} **落とし穴 2: ファイルダウンロードが途中で切れる** `StreamingHttpResponse` を使ったファイルダウンロードが途中で切れる問題があります。 原因は Nginx の `proxy_buffering on`(デフォルト)がレスポンス全体をバッファしようとし、`proxy_read_timeout` 内にバッファが完了しないとタイムアウトすることにあります。 これは{numref}`HTTP は何をやりとりしているのか`({ref}`HTTP は何をやりとりしているのか`)で学んだ `Transfer-Encoding: chunked` と、{numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)で学んだ WSGI のイテラブル設計、そしてリバースプロキシの挙動を組み合わせて初めて理解できる問題です。 ``` ```{warning} **落とし穴 3: リクエストボディの二重読み** `request.body` を2回読もうとして2回目が空になるバグも頻出します。 {numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)で解説した通り、`wsgi.input` はストリームであり、一度 `.read()` すると位置が末尾に移動します。 フレームワークが提供する `request.body` プロパティはキャッシュ機構を持つことが多いですが、ミドルウェアで先に消費された場合や、ASGI 環境で挙動が異なる場合に問題が顕在化します。 ``` これらの落とし穴に共通するのは、抽象化の境界を越えたところで問題が発生しているという点です。 フレームワークの内側だけを知っている開発者は、問題がフレームワークの外で起きていることに気づけず、ビュー関数やテンプレートを何度見直しても解決に至りません。 ```{important} 本書の{numref}`HTTP は何をやりとりしているのか`({ref}`HTTP は何をやりとりしているのか`)から{numref}`WSGI の上に何が必要になるのか`({ref}`WSGI の上に何が必要になるのか`)で積み上げてきた知識は、まさにこの状況に備えるためのものです。 - TCP ソケットの挙動を知っていれば接続の問題を切り分けられる - HTTP の構造を知っていればヘッダーやボディの異常を特定できる - WSGI の仕組みを知っていればサーバとアプリの境界で何が起きているかを推測できる 抽象化の恩恵を享受しながら、必要なときに一段下の層へ降りて調査できること、それが本書の目指す「トラブルシューティングに強い開発者」の姿です。 ``` --- 第1部はここで完結です。 TCP ソケットから始まり、HTTP の構造、自作サーバ、WSGI の仕様、そして WSGI フレームワークの内部構造までを一気通貫で学びました。 第2部では、この知識を土台にして Django と FastAPI の内部へ踏み込みます。 Vol.2「Django を WSGI 視点で見る」では Django の WSGI ハンドラがリクエストを受け取り、ミドルウェアチェーンを通過してビュー関数に届くまでの経路を、ソースコードレベルで追跡します。 (ch05-現場で起きる問題)= ## 現場で起きる問題 ### middleware の順番 ここにダミーの内容が入ります。 ### body の二重読み ここにダミーの内容が入ります。 ### request context の誤解 ここにダミーの内容が入ります。 ### デバッグサーバを本番利用する危険 ここにダミーの内容が入ります。