5. WSGI の上に何が必要になるのか
4 章(WSGI が生まれた背景)では、WSGI の仕様を学び、最小限の WSGI アプリケーションを自分の手で書きました。
environ からリクエスト情報を取り出し、start_response でステータスとヘッダーを返し、イテラブルでボディを返す——WSGI の仕組みは理解できましたが、同時に「これをそのまま使い続けるのはつらい」という感覚も得たのではないでしょうか。
本章では、その「つらさ」を具体的に整理します。 生の WSGI で何が不便なのかを明確にすることで、フレームワークが何を解決しているのかが見えてきます。 フレームワークの便利な機能をただ使うのではなく、「なぜこの機能が存在するのか」を理解することが、本章の目的です。
5.1. 生の WSGI がつらい理由
前章の JSON API の例を振り返ってみましょう。 ユーザー一覧を返すだけのエンドポイントでも、こんなコードを書く必要がありました。
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() を書かなければなりません。
警告
この冗長さは、ミスの温床でもあります。
Content-Lengthの計算を間違えるエンコーディング前の文字列でバイト数を計算してしまう
start_responseを呼び忘れるイテラブルではなくバイト列を直接返してしまう
これらの問題は、同じコードを何度も手で書くからこそ起きるものです。
フレームワークは、この繰り返しとミスの可能性を排除するために存在します。 具体的に何を解決してくれるのかを、以下の4つの領域に分けて見ていきます。
領域 |
生の WSGI の問題 |
フレームワークの解決策 |
|---|---|---|
request / response 抽象化 |
|
|
ルーティング |
|
宣言的なパターンマッチング |
ミドルウェア |
|
リクエスト/レスポンスオブジェクトを扱う形式 |
例外ハンドリング |
各エンドポイントでの |
システム全体の一元管理 |
5.2. request / response 抽象化
生の WSGI では、リクエスト情報は environ 辞書にフラットに格納されています。
メソッドは environ["REQUEST_METHOD"]、パスは environ["PATH_INFO"]、ヘッダーは HTTP_ プレフィックス付きのキー、ボディは environ["wsgi.input"] のファイルライクオブジェクト——情報が存在する場所を知っていれば取り出せますが、直感的とは言いがたいインタフェースです。
フレームワークは、この environ 辞書をリクエストオブジェクトに変換します。
# 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
注釈
フレームワークのリクエストオブジェクトは、environ のキー名の変換規則を隠蔽し、CONTENT_LENGTH の空文字列チェックを引き受け、wsgi.input からの読み取りを管理してくれます。
開発者は request.body と書くだけです。
レスポンス側も同様です。
生の WSGI では start_response を呼び、Content-Length を自分で計算し、バイト列のイテラブルを返す必要がありました。
フレームワークはレスポンスオブジェクトを提供し、これらの手続きをカプセル化します。
# 生の 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 の手続きに変換されています。
5.3. ルーティング
4-6 の JSON API の例で、パスの分岐を if ... elif ... else で書きました。
パスが静的な文字列の比較だけであればなんとかなりますが、/users/42 のような動的なパスパラメータが入ると途端に厄介になります。
# 生の 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のように数値でない値が入る場合
フレームワークのルーティングシステムは、この問題を宣言的に解決します。
# Django
path("users/<int:user_id>/", views.user_detail)
# Flask
@app.route("/users/<int:user_id>")
def user_detail(user_id): ...
# FastAPI
@app.get("/users/{user_id}")
def user_detail(user_id: int): ...
Tip
パスのパターンを宣言し、パラメータの名前と型を指定するだけで、マッチング・パラメータの抽出・型変換が自動的に行われます。 パターンに一致しなければ 404 が返り、型変換に失敗すれば適切なエラーレスポンスが返ります。
ルーティングシステムはさらに、次のような機能も提供します。
URL の逆引き(名前から URL を生成する)
正規表現によるパターンマッチ
名前空間によるルートのグループ化
これらをすべて if ... elif で実装しようとすると、ルーティングだけでコードの大半を占めることになってしまいます。
5.4. ミドルウェア
1-3 で「玉ねぎの皮」モデルとして紹介したミドルウェアは、WSGI の上に構築されるフレームワークの重要な機能です。
WSGI 自体にもミドルウェアの概念はあります。
サーバから見ればアプリケーション、アプリケーションから見ればサーバとして振る舞う callable を間に挟むことができます。
しかし、生の WSGI ミドルウェアは environ と start_response を直接操作するため、書くのも読むのも煩雑です。
フレームワークは、ミドルウェアをより書きやすい形に抽象化しています。 Django であれば、リクエストオブジェクトとレスポンスオブジェクトを受け取るクラスとして書けます。
# 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 トークンの検証、リクエストログの記録——こうした横断的な処理を、アプリケーションコードとは独立した場所に整理できます。
5.5. 例外ハンドリング
生の WSGI アプリケーションでは、例外処理はすべて自分で書く必要があります。
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 レスポンスに変換します
重要
開発者は、ビジネスロジックで例外が発生したときに適切な例外クラスを投げるだけです。 その例外を HTTP レスポンスに変換する作業は、フレームワークが引き受けてくれます。
本節では、生の WSGI がつらい理由を request / response の抽象化、ルーティング、ミドルウェア、例外ハンドリングという4つの領域に分けて整理しました。 これらはすべて、WSGI の仕様自体には含まれていない「WSGI の上に必要なもの」です。
次節からは、これらの「必要なもの」を実際にどう提供しているかを、Werkzeug と Bottle という2つのライブラリの内部を見ながら確認していきます。
どちらも WSGI の environ と start_response の上に薄い抽象層を重ねたものであり、フレームワークの本質を理解するのに最適な教材です。
5.6. Werkzeug を理解する
前節で、生の WSGI の上に何が必要かを整理しました。 本節では、その「必要なもの」を提供しているライブラリのひとつである Werkzeug の内部を見ていきます。
Werkzeug は Flask の基盤として広く知られていますが、Flask とは独立したライブラリです。 Werkzeug を理解することは、Flask を理解することであり、さらに言えば「WSGI フレームワークが最低限何をしているのか」を理解することでもあります。
5.6.1. Werkzeug の立ち位置
Werkzeug(ドイツ語で「工具」を意味する)は、自らを「WSGI ユーティリティライブラリ」と位置づけています。 フレームワークそのものではなく、フレームワークを作るための部品集です。
この位置づけを理解するために、WSGI エコシステムの層構造を整理しましょう。
WSGI サーバ(Gunicorn, uWSGI)
↓ environ / start_response
WSGI ユーティリティ(Werkzeug)
↓ Request / Response オブジェクト, ルーティング
フレームワーク(Flask)
↓ デコレータ, テンプレート, 設定管理
アプリケーションコード(あなたのコード)
WSGI サーバとアプリケーションコードの間に、Werkzeug と Flask が重なっています。
Werkzeug: WSGI の
environとstart_responseを扱いやすいオブジェクトに変換する層Flask: その上にルーティングのデコレータ構文やテンプレートエンジンの統合などの「開発者体験」を追加する層
Werkzeug が提供する主要な機能は次の通りです。
機能 |
役割 |
|---|---|
|
|
URL ルーティング |
パスのパターンマッチングと型変換 |
デバッガ |
ブラウザ上でのインタラクティブなスタックトレース |
開発用サーバ |
手軽なローカル開発環境 |
Tip
Werkzeug のコードを読むと、environ 辞書をどう加工しているか、start_response をどうラップしているかが見えるため、「WSGI の上にフレームワークを作るとはどういうことか」を具体的に理解できます。
5.6.2. Request / Response オブジェクト
Werkzeug の最も基本的な貢献は、environ 辞書を Request オブジェクトに、レスポンスの組み立てを Response オブジェクトに変換することです。
まず、Request オブジェクトを見てみましょう。
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 オブジェクトも同様に、レスポンスの組み立てを簡潔にします。
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 が内部で行い、エンコーディングも自動です。
注釈
注目すべきは最後の行 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 ユーティリティ」の立ち位置です。
5.6.3. URL routing
Werkzeug は、URL ルーティングの仕組みも提供しています。
Map と Rule を使って、パスのパターンとエンドポイント名の対応を定義します。
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/<int:user_id>", 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/<int:user_id>", endpoint="user_detail") という記述で、/users/42 のようなパスにマッチし、42 を整数として user_id に取り出すルールが定義されます。
adapter.match() がマッチングを実行し、エンドポイント名と抽出されたパラメータを返します。
4 章(WSGI が生まれた背景)で path.startswith("/users/") と int(path.split("/")[2]) を組み合わせて苦労していたパスパラメータの処理が、<int:user_id> という宣言的な記法で解決されています。
パスに一致しなければ NotFound 例外が、メソッドが許可されていなければ MethodNotAllowed 例外が発生するため、404 と 405 の区別も自然に行えます。
コラム: Flask のルーティングと Werkzeug の関係
Flask の @app.route("/users/<int:user_id>") デコレータは、この Werkzeug のルーティングシステムの上に構文糖を載せたものです。
Flask は @app.route() が呼ばれるたびに内部で Rule オブジェクトを作成し、Map に追加しています。
デコレータの裏側で行われているのは、ここで見たのと本質的に同じ処理です。
なお、Django のルーティングシステムは Werkzeug とは独立に開発されたもので、内部構造は異なります。 しかし、「パスのパターンを定義し、リクエストのパスと照合し、パラメータを抽出する」という責務は同じです。
5.6.4. debugger, reloader への軽い言及
Werkzeug は、Request / Response とルーティングの他にも、開発を支援する2つの重要な機能を提供しています。 デバッガとリローダです。
Werkzeug のデバッガは、アプリケーションで未処理の例外が発生したとき、ブラウザ上にインタラクティブなスタックトレースを表示します。 各フレームの変数を確認でき、ブラウザ上で Python のコードを実行することすらできます。 Flask の開発サーバで見かけるあの詳細なエラーページは、Werkzeug のデバッガが生成しているものです。
この機能は WSGI ミドルウェアとして実装されています。 アプリケーションを Werkzeug のデバッガミドルウェアでラップすると、アプリケーション内で例外が発生した場合にミドルウェアがそれをキャッチし、スタックトレースを HTML としてレスポンスに含めます。 WSGI のミドルウェアの仕組み——アプリケーションの前後に処理を挟む——を活用した実装です。
リローダは、ソースコードの変更を監視し、変更が検出されたらサーバプロセスを自動的に再起動する機能です。
Flask の app.run(debug=True) で有効になるコード変更時の自動リロードは、Werkzeug のリローダが担当しています。
危険
デバッガとリローダは、本番環境では絶対に使ってはいけません。
デバッガ: ブラウザ上で任意の Python コードを実行できるため、本番で有効にすると深刻なセキュリティ上の脆弱性になります
リローダ: パフォーマンスのオーバーヘッドがあり、本番環境には不適切です
DEBUG = True や debug=True を本番環境で設定しないことは、Web 開発の基本的なセキュリティプラクティスです。
本節では、Werkzeug が WSGI の上にどのような抽象層を提供しているかを確認しました。
environ辞書をRequestオブジェクトに変換するレスポンスの組み立てを
Responseオブジェクトに委ねるMapとRuleでルーティングを宣言的に記述するデバッガとリローダで開発体験を向上させる
これらはすべて、WSGI の environ と start_response の上に構築されています。
次節では、Werkzeug とは異なるアプローチで WSGI の上に抽象層を構築した Bottle の内部を覗きます。 Bottle はフレームワーク全体が1つのファイルに収まるという特徴を持ち、フレームワークの全体像を一望できる貴重な教材です。
5.7. Bottle を理解する
前節では Werkzeug が WSGI の上にユーティリティ層を構築するアプローチを見ました。 本節では、まったく異なる設計哲学を持つ Bottle を取り上げます。
Bottle は「フレームワーク全体が1つの Python ファイルに収まる」という特徴を持つマイクロフレームワークです。 この極端なコンパクトさが、フレームワークの内部構造を学ぶうえで大きな利点になります。 Werkzeug が部品集として個々の機能を提供するのに対し、Bottle はルーティング、リクエスト処理、レスポンス生成、テンプレートエンジンまでをひとつのファイルの中で完結させています。
5.7.1. 単一ファイル志向の軽さ
Bottle のソースコードは bottle.py という単一のファイルです。
外部ライブラリへの依存はゼロで、Python の標準ライブラリだけで動作します。
pip install bottle
インストールすると、bottle.py が1ファイルだけ配置されます。
このファイルの中に、フレームワークのすべてが入っています。
機能 |
説明 |
|---|---|
ルーティングシステム |
パスパターンのマッチングと型変換 |
リクエストオブジェクト |
|
レスポンスオブジェクト |
ステータス・ヘッダー・ボディの管理 |
テンプレートエンジン |
簡易的な HTML テンプレート |
開発用サーバ |
WSGI アダプタとサーバ起動機能 |
コラム: 単一ファイルで学ぶフレームワーク設計
Django のソースコードは数百のファイル、数万行に及びます。 Flask も Werkzeug を含めると相当な規模になります。 しかし Bottle であれば、ファイルを上から下まで読むだけで、フレームワークが何をしているかの全体像が見えます。
この設計哲学は、Bottle の作者 Marcel Hellkamp が「フレームワークの全体像を1ファイルで把握できること」を重視した結果です。
Bottle で最小のアプリケーションを書いてみましょう。
# app.py
from bottle import route, run
@route("/")
def index():
return "Hello, Bottle!"
@route("/greet/<name>")
def greet(name):
return f"Hello, {name}!"
run(host="127.0.0.1", port=8000)
python app.py
これだけで、ルーティング付きの Web アプリケーションが動きます。
curl http://127.0.0.1:8000/greet/Taro にアクセスすれば Hello, Taro! が返ってきます。
このコードの裏側では次のことが行われています。
@route("/")デコレータが、パスのパターンとビュー関数の対応を Bottle の内部ルーティングテーブルに登録するrun()が開発用の WSGI サーバを起動するリクエストが届くたびに Bottle の WSGI アプリケーションが呼び出される
environを受け取り、ルーティングテーブルを検索し、一致したビュー関数を呼び出し、戻り値をレスポンスに変換して返す
5.7.2. デコレータベースのルーティング
Bottle のルーティングは、デコレータ構文で宣言的に定義します。 このスタイルは後に Flask にも採用され、Python Web フレームワークの定番の書き方になりました。
from bottle import get, post, request
@get("/users")
def user_list():
return {"users": ["Taro", "Hanako"]}
@get("/users/<user_id:int>")
def user_detail(user_id):
return {"id": user_id, "name": "Taro"}
@post("/users")
def create_user():
data = request.json
return {"created": data}
注釈
@get と @post は、それぞれ GET リクエストと POST リクエストにのみ反応するデコレータです。
@route デコレータに method 引数を渡す形式もありますが、@get/@post のほうが意図が明確です。
<user_id:int> はパスパラメータの宣言で、:int はフィルタと呼ばれる型変換の指定です。
Werkzeug の <int:user_id> と記法は異なりますが、やっていることは同じ——パスの該当部分を整数に変換し、ビュー関数の引数として渡します。
変換に失敗した場合(たとえば /users/abc)は 404 が返ります。
このデコレータの内部で何が起きているかを、概念的に示すと次のようになります。
# @get("/users/<user_id:int>") の疑似的な内部動作
def get(path):
def decorator(func):
# ルーティングテーブルにエントリを追加
app.routes.append({
"method": "GET",
"pattern": compile_pattern(path), # パスをパターンに変換
"callback": func,
})
return func
return decorator
デコレータは関数を受け取り、その関数をルーティングテーブルに登録して、元の関数をそのまま返します。
リクエストが届いたとき、Bottle はルーティングテーブルを走査し、パスとメソッドが一致するエントリを見つけ、対応する callback を呼び出します。
4 章(WSGI が生まれた背景)で if path == "/users" and method == "GET": と書いていた分岐が、デコレータの裏側で自動的に構築されるルーティングテーブルに置き換わっているのです。
Bottle のビュー関数の戻り値が柔軟なのも特徴的です。
戻り値の型 |
レスポンスの形式 |
|---|---|
文字列 |
|
辞書 |
|
|
そのまま使用 |
# 文字列 → text/html レスポンス
@get("/html")
def html():
return "<h1>Hello</h1>"
# 辞書 → application/json レスポンス
@get("/json")
def json_example():
return {"message": "hello"}
この「戻り値の型を見てレスポンスの形式を自動判定する」仕組みは、ビュー関数の中で start_response の呼び出しや Content-Type の設定を意識しなくて済むようにするためです。
Bottle の内部では、ビュー関数の戻り値を検査し、文字列であればエンコードしてボディにし、辞書であれば json.dumps() でシリアライズし、最終的に WSGI のイテラブルに変換しています。
5.7.3. WSGI アプリとしての Bottle
Bottle の最も重要な性質は、Bottle アプリケーション自体が WSGI アプリケーションであるという点です。
from bottle import Bottle
app = Bottle()
@app.get("/")
def index():
return "Hello!"
この app は WSGI の callable です。app(environ, start_response) と呼び出すことができます。つまり、Gunicorn の上でそのまま動かせます。
gunicorn app:app
Bottle のソースコードの中で、この WSGI callable としての動作を担うのが Bottle.__call__ メソッドです。概念的に単純化すると、次のような処理が行われています。
# 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)
このフローは、4 章(WSGI が生まれた背景)で自分の手で書いた WSGI アプリケーションの構造と本質的に同じです。
environを受け取るパスとメソッドでルーティングする
ハンドラーを実行する
結果をレスポンスに変換する
WSGI の形式で返す
Bottle はこの一連の処理を、デコレータと型の自動判定で開発者にとって書きやすくしているのです。
Bottle が WSGI アプリケーションであるということは、WSGI ミドルウェアをそのまま適用できることも意味します。
from bottle import Bottle
app = Bottle()
@app.get("/")
def index():
return "Hello!"
# WSGI ミドルウェアを適用
from some_middleware import SomeMiddleware
wrapped_app = SomeMiddleware(app)
重要
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 ミドルウェアの仕組みへと進みます。
5.8. WSGI ミドルウェアを書く
前節まで、Werkzeug と Bottle が WSGI の上にどのような抽象層を構築しているかを見てきました。 本節では、WSGI のもうひとつの重要な概念——ミドルウェア——を、自分の手で書きながら理解します。
1-3 で「玉ねぎの皮」モデルとして紹介したミドルウェアの仕組みが、WSGI ではどう実現されているのか。 その構造は驚くほどシンプルです。
5.8.1. callable を包む構造
WSGI ミドルウェアの本質は、ある WSGI アプリケーションを別の WSGI アプリケーションで包むことです。
WSGI アプリケーションは application(environ, start_response) という形式の callable です。
ミドルウェアは、この callable を受け取り、自身も同じ形式の callable として振る舞います。
サーバから見ればミドルウェアはアプリケーションであり、アプリケーションから見ればミドルウェアはサーバです。
構造を疑似コードで表すと、こうなります。
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 として振る舞います。
自分の処理を挟みつつ、最終的には元のアプリケーションに処理を委譲する——これがミドルウェアのすべてです。
ミドルウェアを適用するのも単純です。
# 元のアプリケーション
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) で呼び出される
複数のミドルウェアを重ねることもできます。
app = application
app = MiddlewareA(app)
app = MiddlewareB(app)
app = MiddlewareC(app)
リクエストとレスポンスは次の順序で流れます。
リクエスト(外側 → 内側): MiddlewareC → MiddlewareB → MiddlewareA → application
レスポンス(内側 → 外側): application → MiddlewareA → MiddlewareB → MiddlewareC
まさに玉ねぎの皮です。
注釈
この仕組みが成立するのは、WSGI がインタフェースを統一しているからです。
すべてのミドルウェアとすべてのアプリケーションが (environ, start_response) → iterable という同じ形式に従っているため、任意の順序で自由に組み合わせられます。
5.8.2. ロギングミドルウェア
具体的なミドルウェアを書いてみましょう。 最初は、すべてのリクエストのメソッド、パス、ステータスコード、処理時間をログに出力するミドルウェアです。
# 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 宣言を使う必要があります。
使い方は簡単です。
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 アプリケーションであれば何にでも適用できます。
5.8.3. ヘッダー追加ミドルウェア
次に、すべてのレスポンスにカスタムヘッダーを追加するミドルウェアを書いてみましょう。 セキュリティ関連のヘッダーを一括で付与するのは、実務でもよくある用途です。
# 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
注釈
Django の SecurityMiddleware も、同様の仕組みでセキュリティヘッダーを追加しています。
Django のミドルウェアは WSGI ミドルウェアとは形式が異なりますが(Django 独自のリクエスト/レスポンスオブジェクトを使う)、「レスポンスが返される前にヘッダーを追加する」という責務は同じです。
5.8.4. 例外処理ミドルウェア
最後に、アプリケーション内で発生した未処理の例外をキャッチし、500 エラーレスポンスを返すミドルウェアを書きます。
# 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 であれば汎用的なエラーメッセージだけを返します。
def application(environ, start_response):
# わざと例外を発生させる
raise ValueError("Something went wrong!")
app = ErrorHandlingMiddleware(application, debug=True)
このアプリケーションにリクエストを送ると、ブラウザには500エラーとスタックトレースが表示されます。 ミドルウェアがなければ、サーバプロセスがクラッシュするか、サーバ固有のエラーページが表示されるかのどちらかです。
注意
ここで注意すべき点がひとつあります。
アプリケーションが 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 がそれらをどう組み合わせてフレームワークとしての体験を提供しているかを確認します。
5.9. Flask の位置づけを整理する
5.9.1. Flask は Werkzeug の上にある
Flask の中核は Werkzeug が提供する部品の組み合わせです。
Flask アプリケーションを作成すると、内部では Werkzeug の Request、Response、Map/Rule(URL ルーティング)、開発用サーバ、デバッガ、リローダがそのまま使われています。
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/users/<int:user_id>")
def user_detail(user_id):
return jsonify({"id": user_id, "name": "Taro"})
この短いコードの裏で起きていることを分解すると次のようになります。
Flask.__init__で Werkzeug のMapが生成される@app.routeデコレータがRuleを追加するリクエストが届くと
Flask.__call__(environ, start_response)が呼ばれる内部で Werkzeug の
Request(environ)を生成し、Map.bind_to_environでルートをマッチングし、対応するビュー関数を呼び出すビューの戻り値は Werkzeug の
Responseオブジェクトに変換され、最終的にresponse(environ, start_response)で WSGI レスポンスとして返却される
重要
Flask は「Werkzeug を直接使う煩雑さを、デコレータとコンテキスト管理で包んだ薄い層」であり、独自の HTTP パーサやルーティングエンジンを持っているわけではありません。
5.9.2. 「ミニフレームワーク」とは何か
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 機構)に集中しています。
5.9.3. 本書で 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 に精通している読者は、5 章(WSGI の上に何が必要になるのか)までの知識を持って Flask のソースコードを読めば、flask/app.py の wsgi_app メソッドが Werkzeug のルーティングとリクエスト/レスポンスをどのように統合しているかを自力で追跡できるはずです。
次節では、WSGI の上に重ねてきた抽象化の層が開発にもたらす利益と、同時に何を見えなくするのかを整理します。
5.10. 抽象化がもたらす利益と見えなくなるもの
2 章(HTTP は何をやりとりしているのか)で TCP ソケットから生バイト列を読み取り、3 章(まずは 1 リクエストだけ処理するサーバを作る)で自前の HTTP サーバを書き、4 章(WSGI が生まれた背景)で WSGI によるサーバとアプリの分離を学び、5 章(WSGI の上に何が必要になるのか)で Werkzeug・Bottle・Flask がその上にリクエスト/レスポンス抽象化やルーティングを重ねる様子を見てきました。 抽象化の層が増えるほど開発は楽になりますが、同時に「何が起きているか」が見えなくなります。 本節ではその利益と代償を整理します。
5.10.1. 開発速度
抽象化の最大の恩恵は、ビジネスロジックへの集中です。 3 章(まずは 1 リクエストだけ処理するサーバを作る)で自作した HTTP サーバでは、ユーザー一覧を JSON で返すだけでも次のすべての処理を毎回書く必要がありました。
ソケット生成
ヘッダー終端検出ループ
Content-Length計算ステータスライン組み立て
バイト列エンコーディング
Flask であれば同じことが数行で済みます。
@app.route("/users")
def user_list():
users = get_users_from_db()
return jsonify(users)
Django であれば JsonResponse(data) の一行で、ステータスコード設定、Content-Type ヘッダー付与、JSON シリアライズ、Content-Length 計算、バイト列変換のすべてが完了します。
重要
開発者が書くコードの量が減るだけでなく、定型処理のバグ(エンコーディングミス、ヘッダー漏れ、接続クローズ忘れなど)も構造的に排除されます。
この効率化は個人開発でもチーム開発でも効果を発揮します。 新しいエンドポイントの追加が数分で終わるため、プロトタイピングの速度が上がり、仕様変更への対応コストも下がります。 LLM を使ったコード生成においても、フレームワークの規約に沿ったコードは生成精度が高く、生のソケット操作を含むコードよりも正確に出力される傾向があります。
5.10.2. 認知負荷の削減
抽象化は「考えなくてよいこと」を増やします。
Flask のビュー関数を書いているとき、開発者は TCP の 3 ウェイハンドシェイクや recv() のバッファサイズを意識する必要がありません。
Django の ORM を使っているとき、SQL のエスケープ処理やコネクションプーリングの詳細を知らなくてもアプリケーションは動きます。
これは単に楽をしているのではなく、人間の作業記憶には限界があるという現実に対応した設計です。 一度に7つ前後の要素しか扱えないとされる認知の制約の中で、次のすべてを同時に考えることは不可能です。
TCP 接続管理
HTTP パース
WSGI プロトコル
ルーティング
認証
ビジネスロジック
レスポンス生成
抽象化は下位層を「信頼できるブラックボックス」に変え、開発者が当面の問題に集中できる環境を作ります。
フレームワークの規約
(Django の urlpatterns、Flask の @app.route、FastAPI の型ヒント)
も認知負荷の削減に寄与しています。パターンが統一されていれば、他人が書いたコードでもエンドポイントの場所、リクエストの受け取り方、レスポンスの返し方が予測でき、コードリーディングの速度が上がります。
5.10.3. 低レイヤ理解の不在による落とし穴
しかし抽象化は、問題が起きたときに牙を剥きます。 正常系では見えなくて構わなかった下位層が、異常系では突然顔を出すからです。
警告
落とし穴 1: 502 Bad Gateway の原因調査
本番環境で 502 Bad Gateway が発生したとき、Django のコードだけを見ても原因は分かりません。
502 は Nginx がアップストリーム(Gunicorn)から正常なレスポンスを受け取れなかったことを意味しますが、その原因は次のようなものが考えられます。
Gunicorn のワーカータイムアウト
アプリケーション内の無限ループ
メモリ不足によるワーカークラッシュ
Unix ソケットのパーミッションエラー
これらはいずれも、フレームワークが隠してくれていた層で起きている問題です。
警告
落とし穴 2: ファイルダウンロードが途中で切れる
StreamingHttpResponse を使ったファイルダウンロードが途中で切れる問題があります。
原因は Nginx の proxy_buffering on(デフォルト)がレスポンス全体をバッファしようとし、proxy_read_timeout 内にバッファが完了しないとタイムアウトすることにあります。
これは2 章(HTTP は何をやりとりしているのか)で学んだ Transfer-Encoding: chunked と、4 章(WSGI が生まれた背景)で学んだ WSGI のイテラブル設計、そしてリバースプロキシの挙動を組み合わせて初めて理解できる問題です。
警告
落とし穴 3: リクエストボディの二重読み
request.body を2回読もうとして2回目が空になるバグも頻出します。
4 章(WSGI が生まれた背景)で解説した通り、wsgi.input はストリームであり、一度 .read() すると位置が末尾に移動します。
フレームワークが提供する request.body プロパティはキャッシュ機構を持つことが多いですが、ミドルウェアで先に消費された場合や、ASGI 環境で挙動が異なる場合に問題が顕在化します。
これらの落とし穴に共通するのは、抽象化の境界を越えたところで問題が発生しているという点です。 フレームワークの内側だけを知っている開発者は、問題がフレームワークの外で起きていることに気づけず、ビュー関数やテンプレートを何度見直しても解決に至りません。
重要
本書の2 章(HTTP は何をやりとりしているのか)から5 章(WSGI の上に何が必要になるのか)で積み上げてきた知識は、まさにこの状況に備えるためのものです。
TCP ソケットの挙動を知っていれば接続の問題を切り分けられる
HTTP の構造を知っていればヘッダーやボディの異常を特定できる
WSGI の仕組みを知っていればサーバとアプリの境界で何が起きているかを推測できる
抽象化の恩恵を享受しながら、必要なときに一段下の層へ降りて調査できること、それが本書の目指す「トラブルシューティングに強い開発者」の姿です。
第1部はここで完結です。 TCP ソケットから始まり、HTTP の構造、自作サーバ、WSGI の仕様、そして WSGI フレームワークの内部構造までを一気通貫で学びました。 第2部では、この知識を土台にして Django と FastAPI の内部へ踏み込みます。 Vol.2「Django を WSGI 視点で見る」では Django の WSGI ハンドラがリクエストを受け取り、ミドルウェアチェーンを通過してビュー関数に届くまでの経路を、ソースコードレベルで追跡します。
5.11. 現場で起きる問題
5.11.1. middleware の順番
ここにダミーの内容が入ります。
5.11.2. body の二重読み
ここにダミーの内容が入ります。
5.11.3. request context の誤解
ここにダミーの内容が入ります。
5.11.4. デバッグサーバを本番利用する危険
ここにダミーの内容が入ります。