4. WSGI が生まれた背景

3 章まずは 1 リクエストだけ処理するサーバを作る)では、ソケットだけで HTTP サーバを自作し、その限界を確認しました。 同時接続ができない、エラー処理が脆弱、HTTP 仕様への完全な準拠が困難——これらはすべて、サーバ部分を自分で書くことの大変さに起因する問題でした。

しかし、もうひとつ見過ごせない問題がありました。 自作サーバでは、ソケットの操作とアプリケーションのロジック(ルーティング、ビジネスロジック、レスポンス生成)が同じコードの中に混在していました。 サーバを別の実装に差し替えたければ、アプリケーション全体を書き直さなければなりません。 この密結合の問題を解決するために生まれたのが WSGI(Web Server Gateway Interface) です。

WSGI は 2003 年に PEP 333 として提案され、2010 年に PEP 3333 として Python 3 に対応する形で改訂されました。 本項では、WSGI がなぜ必要とされたのか、その歴史的な背景を追いかけていきます。

4.1. CGI の問題

WSGI の前身とも言える仕組みが CGI(Common Gateway Interface) です。

diagram

CGI は 1993 年に登場した、Web サーバから外部プログラムを呼び出すための仕様です。

CGI の動作はシンプルです。

  • Web サーバ(Apache など)が HTTP リクエストを受け取ると、リクエストの情報を環境変数に設定する

  • 外部プログラム(CGI スクリプト)を新しいプロセスとして起動する

  • CGI スクリプトは環境変数からリクエスト情報を読み取り、処理結果を標準出力に書き出す

  • Web サーバはその標準出力を HTTP レスポンスとしてクライアントに返す

Python の CGI スクリプトは、たとえば次のような姿をしていました。

#!/usr/bin/env python
import os

method = os.environ.get("REQUEST_METHOD", "GET")
path = os.environ.get("PATH_INFO", "/")
query = os.environ.get("QUERY_STRING", "")

print("Content-Type: text/html")
print()  # 空行(ヘッダーの終わり)
print(f"<html><body>")
print(f"<p>Method: {method}</p>")
print(f"<p>Path: {path}</p>")
print(f"<p>Query: {query}</p>")
print(f"</body></html>")

os.environ からリクエスト情報を取得し、print() でレスポンスを出力します。 自作サーバで recv()sendall() を使って行っていたことを、環境変数と標準出力という別のインタフェースで行っている、とも言えます。 CGI は、Web サーバとアプリケーションを分離するという意味では、正しい方向への一歩でした。

警告

CGI には致命的なパフォーマンス上の問題があります。リクエストのたびに新しいプロセスが起動されるのです。

プロセスの起動は、OS にとって軽い操作ではありません。次の処理がリクエストごとに毎回実行されます。

  • メモリの確保

  • Python インタプリタの起動

  • モジュールのインポート

  • スクリプトのパース

小規模なサイトではこれでも動きましたが、リクエスト数が増えるとプロセスの起動コストがボトルネックになります。

mod_python(Apache に Python インタプリタを組み込むモジュール)や FastCGI(プロセスを永続化して再利用する仕組み)など、CGI のパフォーマンス問題を解決する技術も登場しました。 しかし、これらはそれぞれ独自のインタフェースを持っていたため、新たな問題を生むことになります。

4.2. Python Web の断片化

2000 年代初頭の Python Web 開発は、混沌とした状況にありました。

Web サーバとアプリケーションの接続方法が統一されていなかったのです。 CGI、mod_python、FastCGI、SCGI——それぞれの仕組みが独自のインタフェースを定めており、アプリケーション側はどのサーバで動かすかに応じてコードを書き分ける必要がありました。

Web フレームワークの側も同様に断片化していました。 Zope、Twisted、Quixote、CherryPy、Webware——それぞれが独自のサーバ連携の仕組みを持っていました。 あるフレームワークで書いたアプリケーションを、別のサーバの上で動かすことは簡単ではありませんでした。

この状況は、フレームワークの開発者にもアプリケーションの開発者にも不幸でした。

  • フレームワーク開発者: 複数のサーバ環境に対応するためのアダプターを書き続けなければならない

  • アプリケーション開発者: サーバやフレームワークの選択が将来のデプロイ環境を制約することを覚悟しなければならない

注釈

PEP 333 の冒頭は、まさにこの問題を指摘しています。

「Python には Web フレームワークが多数存在するが、フレームワークの選択が使用可能な Web サーバを制限し、その逆もまた然りである」

Java の世界ではサーブレット API が、この問題を早い段階で解決していました。 Python にも同じような共通インタフェースが必要だ——これが WSGI の出発点です。

4.3. 共通インタフェースの必要性

3 章まずは 1 リクエストだけ処理するサーバを作る)で自作サーバを書いた経験を思い出してください。 handle_request() 関数は、ソケットから受け取ったバイト列をパースし、パスとメソッドに基づいて分岐し、レスポンスを組み立てて返していました。 サーバの仕事(ソケット操作、HTTP パース)とアプリケーションの仕事(ルーティング、ビジネスロジック)が一体化していました。

もし「自作サーバのアプリケーション部分を、Gunicorn の上でもそのまま動かしたい」と思ったら、どうすればよいでしょうか。 自作サーバは handle_request(header_part, body) という独自のインタフェースでアプリケーションを呼び出していますが、Gunicorn はそのインタフェースを知りません。 Gunicorn が理解できる形でアプリケーションを書き直す必要があります。

ここで、両者の間に共通の約束事があったらどうでしょう。 「サーバはリクエスト情報をこういう形式でアプリケーションに渡す。アプリケーションはレスポンスをこういう形式でサーバに返す。」——この約束事さえ守れば、どのサーバの上でもアプリケーションが動き、どのアプリケーションも任意のサーバの上で動きます。

サーバ A ─┐                ┌─ アプリケーション X
サーバ B ─┼─ [ 共通IF ] ─┼─ アプリケーション Y
サーバ C ─┘                └─ アプリケーション Z
diagram

Tip

共通インタフェースの効果は組み合わせ数で実感できます。

  • 共通インタフェースがない場合: サーバ 3 種類 × アプリ 3 種類 = 最大 9 種類のアダプターが必要

  • 共通インタフェースがある場合: 各サーバ 1 つ + 各アプリ 1 つ = 合計 6 つの実装だけで全組み合わせが動く

サーバやアプリケーションの数が増えるほど、この効果は大きくなります。

WSGI が定めたのは、まさにこの共通インタフェースです。 そしてその設計は驚くほどシンプルです。 アプリケーション側の要件は「2つの引数を受け取る callable であること」、それだけです。

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello, World!"]
  • environ: リクエスト情報を格納した辞書

  • start_response: ステータスコードとヘッダーをサーバに伝えるための callable

この3行が、WSGI に準拠した最小のアプリケーションです。

コラム: CGI の設計を受け継ぐ WSGI

CGI の設計が色濃く反映されていることに気づいたでしょうか。

項目

CGI

WSGI

リクエスト情報の受け取り方

os.environ(環境変数)

environ 辞書

レスポンスの返し方

print()(標準出力)

start_response + イテラブル

実行方式

プロセスを毎回起動

callable として呼び出す

CGI の概念を引き継ぎつつ、プロセスを毎回起動するのではなく、Python の callable として呼び出す——これが WSGI の本質的なアイデアです。 CGI のインタフェース設計の良さを活かしながら、パフォーマンス上の問題を解消したのです。

この仕様によって、Django は WSGIHandler クラスを通じて任意の WSGI サーバ上で動くようになりました。 Flask は Werkzeug を介して WSGI に準拠しています。 Gunicorn、uWSGI、mod_wsgi——これらの WSGI サーバは、どのフレームワークのアプリケーションでも動かすことができます。 Python の Web エコシステムが断片化から統合へ向かう転換点が、WSGI だったのです。


本項では、WSGI が生まれた背景を3つの段階で追いかけました。

  1. CGI の「リクエストごとにプロセスを起動する」というパフォーマンス問題

  2. サーバとフレームワークが乱立し組み合わせの自由がなかった Python Web の断片化

  3. 共通インタフェースによってサーバとアプリケーションを疎結合にするという解決策

次項では、WSGI の仕様そのものに踏み込みます。 environ 辞書には何が入っているのか、start_response はどう呼び出すのか、イテラブルとしてのレスポンスボディはどう設計されているのか——仕様を読み解きながら、自分の手で WSGI アプリケーションを書いていきましょう。

4.4. WSGI の全体像

前項で、WSGI が生まれた背景——CGI のパフォーマンス問題、Python Web エコシステムの断片化、共通インタフェースの必要性——を確認しました。 本項では、WSGI の仕様そのものに踏み込み、サーバとアプリケーションがどのように連携するかの全体像を把握します。

WSGI の仕様(PEP 3333)は決して長い文書ではありませんが、初めて読むと抽象的に感じるかもしれません。 本項では、3 章まずは 1 リクエストだけ処理するサーバを作る)で自作サーバを書いた経験と対比させながら、WSGI の設計を具体的に理解していきます。

4.4.1. Web サーバ / WSGI サーバ / アプリの接点

WSGI の世界には、2つの役割が登場します。 サーバ(またはゲートウェイ)アプリケーション(またはフレームワーク) です。

  • サーバ側の責務: TCP ソケットの操作、HTTP リクエストのパース、HTTP レスポンスの送信(Gunicorn や uWSGI が担う)

  • アプリケーション側の責務: リクエストの内容に基づいて処理を行い、レスポンスを生成する(Django や Flask が担う)

ここで「WSGI サーバ」と「Web サーバ」の関係を整理しておきましょう。 本番環境の典型的な構成では、クライアントからのリクエストはまず Nginx(Web サーバ / リバースプロキシ)が受け、それを Gunicorn(WSGI サーバ)に転送し、Gunicorn が WSGI インタフェースを通じて Django(アプリケーション)を呼び出します。

クライアント → Nginx(Webサーバ)→ Gunicorn(WSGIサーバ)→ Django(アプリケーション)
diagram

注釈

WSGI が規定しているのは、Gunicorn と Django の間の「接点」の仕様だけです。

  • Nginx と Gunicorn の間: HTTP(または Unix ソケット)で通信 → WSGI の範囲外

  • Gunicorn と Django の間: WSGI インタフェースで接続 → WSGI の守備範囲

開発環境では、この構成がもっとシンプルになります。 Django の manage.py runserver は、簡易的な WSGI サーバと Django アプリケーションを一体化して起動します。 Nginx は介在しません。 しかし、内部的には WSGI インタフェースを通じてリクエストを処理しているため、本番環境で Gunicorn に切り替えても Django のアプリケーションコードは一切変更する必要がありません。 これが共通インタフェースの力です。

3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバに当てはめると、ソケット操作と HTTP パースを行う部分が「WSGI サーバ」に相当し、handle_request() 以降の処理が「WSGI アプリケーション」に相当します。 自作サーバではこの2つが同じファイルの中に書かれていましたが、WSGI はこの2つの間に明確な境界線を引きます。

4.4.2. アプリケーション callable という考え方

WSGI のアプリケーション側の仕様は、驚くほどシンプルです。 アプリケーションは、2つの引数を受け取る callable でなければならない。それだけです。

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello, World!"]

これが WSGI アプリケーションの最小形です。 この3行に、WSGI の仕様の核心がすべて詰まっています。ひとつずつ見ていきましょう。

callable であること。 Python における callable とは、「呼び出し可能なオブジェクト」のことです。

Tip

WSGI は「関数でなければならない」とは言っていません。「callable でなければならない」と言っています。

  • 関数は callable です

  • __call__ メソッドを持つクラスのインスタンスも callable です

この区別は重要で、Django の WSGIHandler はクラスのインスタンスとして WSGI アプリケーションを実装しています。

# 関数として実装
def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello, World!"]

# クラスとして実装
class Application:
    def __call__(self, environ, start_response):
        start_response("200 OK", [("Content-Type", "text/plain")])
        return [b"Hello, World!"]

application = Application()

どちらも WSGI アプリケーションとして有効です。 サーバから見れば、どちらも application(environ, start_response) と呼び出せる callable です。

第1引数 environ は辞書である。 environ には、リクエストの情報がキーと値のペアとして格納されています。

  • CGI の環境変数に由来するキー: REQUEST_METHODPATH_INFOQUERY_STRINGHTTP_HOST など

  • WSGI 独自のキー: wsgi.inputwsgi.errors など

3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバで parse_request_line()parse_headers() を使って取り出していた情報が、WSGI ではすでにパース済みの状態で environ 辞書に入っているのです。 environ の詳細は次項で掘り下げます。

第2引数 start_response は callable である。 サーバがアプリケーションを呼び出すときに、start_response という callable を渡してきます。 アプリケーションはレスポンスボディを返す前に、この start_response を呼び出して、ステータスコードとレスポンスヘッダーをサーバに伝えます。

start_response("200 OK", [("Content-Type", "text/plain")])
  • 第1引数: ステータス文字列("200 OK"

  • 第2引数: ヘッダーのリスト(タプルのリスト)

ヘッダーを辞書ではなくタプルのリストにしているのは、同じ名前のヘッダーが複数回出現しうるためです。

戻り値はイテラブルである。 アプリケーションは、レスポンスボディをバイト列のイテラブルとして返します。 最もシンプルなのはバイト列を要素とするリストです。

return [b"Hello, World!"]

リストではなくジェネレータを返すこともできます。 これにより、大きなレスポンスボディをメモリに一度に載せることなく、少しずつ生成してクライアントに送ることが可能になります。

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    yield b"Hello, "
    yield b"World!"

WSGI サーバの側から見ると、処理の流れは次のようになります。

diagram
# WSGIサーバ側の処理の概略(疑似コード)
environ = build_environ(raw_request)  # リクエストをパースして辞書にする

response_started = False
status = None
response_headers = None

def start_response(status_str, headers):
    nonlocal response_started, status, response_headers
    status = status_str
    response_headers = headers
    response_started = True

result = application(environ, start_response)  # アプリ呼び出し

# ステータスとヘッダーをHTTPレスポンスとして送信
send_status_line(status)
send_headers(response_headers)

# ボディを送信
for chunk in result:
    send_body(chunk)

3 章まずは 1 リクエストだけ処理するサーバを作る)で書いた自作サーバの処理フローと見比べてみてください。 ソケットの操作(acceptrecvsendall)はサーバ側に閉じ込められ、アプリケーション側は environstart_response という抽象的なインタフェースだけを通じてサーバとやりとりしています。 ソケットの存在すら知らないのです。


本項では、WSGI の全体像を把握しました。 サーバとアプリケーションの間に明確な境界線を引き、その接点を「callable に environstart_response を渡し、イテラブルを受け取る」というシンプルな約束事で定義する——この約束事が、Python の Web エコシステム全体を支える基盤です。

次項では、environ 辞書の中身を詳しく見ていきます。 3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバで parse_request_line()parse_headers() を使って苦労して取り出していた情報が、environ の中にどのような形で格納されているかを確認しましょう。

4.5. environ を読む

前項で、WSGI アプリケーションが environstart_response の2つの引数を受け取る callable であることを確認しました。 本項では、第1引数の environ 辞書の中身を詳しく見ていきます。

environ は、サーバがリクエストをパースして組み立てた情報の集合体です。 3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバでは、parse_request_line()parse_headers()parse_full_path() と複数の関数を使ってバイト列から情報を取り出していました。 WSGI では、その作業をサーバが済ませたうえで、すべてをひとつの辞書に詰めてアプリケーションに渡してくれます。

4.5.1. リクエスト情報の入り口

environ の中身を実際に見てみるのが最も早い理解の方法です。

diagram

次の WSGI アプリケーションは、environ のキーと値をすべて表示します。

# environ_dump.py
def application(environ, start_response):
    lines = []
    for key in sorted(environ):
        value = environ[key]
        lines.append(f"{key}: {value!r}")
    body = "\n".join(lines)

    start_response("200 OK", [
        ("Content-Type", "text/plain; charset=utf-8"),
    ])
    return [body.encode("utf-8")]

Python の標準ライブラリに含まれる wsgiref モジュールを使えば、このアプリケーションをすぐに動かせます。

# run_environ_dump.py
from wsgiref.simple_server import make_server
from environ_dump import application

server = make_server("127.0.0.1", 8000, application)
print("Serving on http://127.0.0.1:8000")
server.serve_forever()

起動して curl http://127.0.0.1:8000/hello?name=Taro にアクセスすると、次のような出力が返ってきます(一部抜粋)。

CONTENT_TYPE: ''
GATEWAY_INTERFACE: 'CGI/1.1'
HTTP_ACCEPT: '*/*'
HTTP_HOST: '127.0.0.1:8000'
HTTP_USER_AGENT: 'curl/8.7.1'
PATH_INFO: '/hello'
QUERY_STRING: 'name=Taro'
REMOTE_ADDR: '127.0.0.1'
REMOTE_HOST: ''
REQUEST_METHOD: 'GET'
SCRIPT_NAME: ''
SERVER_NAME: '127.0.0.1'
SERVER_PORT: '8000'
SERVER_PROTOCOL: 'HTTP/1.1'
SERVER_SOFTWARE: 'WSGIServer/0.2'
wsgi.errors: <_io.TextIOWrapper name='<stderr>' ...>
wsgi.input: <_io.BufferedReader ...>
wsgi.multiprocess: False
wsgi.multithread: True
wsgi.run_once: False
wsgi.url_scheme: 'http'

CGI に由来するキー(大文字)と WSGI 独自のキー(wsgi. プレフィックス)が混在していることがわかります。 これらのキーを、重要なものから順に見ていきましょう。

注釈

environ の主なキー一覧を以下にまとめます。

キー

由来

内容

REQUEST_METHOD

CGI

"GET""POST" などのメソッド文字列

PATH_INFO

CGI

リクエストパス("/hello" など)

SCRIPT_NAME

CGI

アプリがマウントされているプレフィックス(通常は空)

QUERY_STRING

CGI

? 以降のクエリ文字列(未パース)

CONTENT_TYPE

CGI

リクエストボディの Content-Type

CONTENT_LENGTH

CGI

リクエストボディのバイト数(文字列)

SERVER_NAME

CGI

サーバのホスト名

SERVER_PORT

CGI

サーバのポート番号(文字列)

HTTP_*

CGI

クライアントが送った HTTP ヘッダー

wsgi.input

WSGI

リクエストボディのストリーム

wsgi.errors

WSGI

エラー出力先

wsgi.url_scheme

WSGI

"http" または "https"

wsgi.multithread

WSGI

マルチスレッド環境かどうか

wsgi.multiprocess

WSGI

マルチプロセス環境かどうか

4.5.2. REQUEST_METHOD

method = environ["REQUEST_METHOD"]  # "GET", "POST" など

HTTP リクエストのメソッドが文字列として格納されています。 3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバでは parse_request_line() でリクエストラインを空白で分割して最初の要素を取り出していましたが、WSGI ではサーバがその作業を済ませて REQUEST_METHOD に入れてくれています。

Django のビュー関数で request.method と書いたとき、Django は内部でこの environ["REQUEST_METHOD"] の値を返しています。 FastAPI の @app.get() デコレータも、内部ではこの値を見てメソッドのマッチングを行っています。

4.5.3. PATH_INFO

path = environ.get("PATH_INFO", "/")  # "/hello", "/users/42" など

リクエストされたパスが格納されています。 3 章まずは 1 リクエストだけ処理するサーバを作る)で parse_full_path() を使ってクエリ文字列を切り離す処理を書きましたが、WSGI ではパスとクエリ文字列が最初から別のキーに分かれています。 PATH_INFO にはパス部分だけが入り、クエリ文字列は QUERY_STRING に入ります。

WSGI にはもうひとつ SCRIPT_NAME というキーがあり、アプリケーションが URL のルート(/)ではなくサブパス(たとえば /myapp)にマウントされている場合に、そのプレフィックスが入ります。 通常は空文字列です。クライアントがリクエストした完全なパスは SCRIPT_NAME + PATH_INFO になります。

# アプリケーションが /myapp にマウントされている場合
# リクエスト: GET /myapp/users/42
environ["SCRIPT_NAME"]  # "/myapp"
environ["PATH_INFO"]    # "/users/42"

この分離は、ひとつのサーバ上で複数の WSGI アプリケーションを異なるパスで同時に運用するときに意味を持ちます。 各アプリケーションは自分の PATH_INFO だけを見ればよく、SCRIPT_NAME のプレフィックスを意識する必要がありません。

4.5.4. QUERY_STRING

query_string = environ.get("QUERY_STRING", "")  # "name=Taro&page=2"

URL の ? 以降のクエリ文字列がそのまま格納されています。 「そのまま」というのは、パースされていない生の文字列だということです。name=Taro&page=2 を辞書に変換する作業は、アプリケーション側(またはフレームワーク)の仕事です。

3 章まずは 1 リクエストだけ処理するサーバを作る)では urllib.parse.parse_qs() を使ってこの変換を行いました。

from urllib.parse import parse_qs
query_params = parse_qs(environ.get("QUERY_STRING", ""))
# {"name": ["Taro"], "page": ["2"]}

注釈

WSGI の environ がクエリ文字列をパースせずに生の文字列のまま渡しているのは、意図的な設計です。 WSGI はサーバとアプリケーションの間の最小限のインタフェースを目指しており、クエリ文字列の解釈方法はアプリケーション側に委ねています。

Django が request.GET として QueryDict オブジェクトを提供し、FastAPI が関数の引数として型変換まで行ってくれるのは、フレームワークがこの生の文字列を加工した結果です。

4.5.5. wsgi.input

body_stream = environ["wsgi.input"]

リクエストボディを読み取るためのファイルライクオブジェクトです。 environ の中で唯一、文字列ではなくオブジェクトが入っているキーのひとつです。

3 章まずは 1 リクエストだけ処理するサーバを作る)では、Content-Length を見てボディのバイト数を判断し、recv() を繰り返し呼んでボディを受信するコードを書きました。 WSGI では、サーバがボディの受信を管理し、アプリケーションにはファイルライクオブジェクトとして提供します。 アプリケーションは .read() メソッドでボディを読み取ります。

content_length = int(environ.get("CONTENT_LENGTH", "0") or "0")
body = environ["wsgi.input"].read(content_length)

警告

CONTENT_LENGTH が空文字列の場合があるため、or "0" でフォールバックしている点に注意してください。 WSGI の仕様では、CONTENT_LENGTH は存在しないか空文字列の場合があり、その場合はボディがないことを意味します。

また、wsgi.input からボディを読み取るときに content_length を指定しないと、ストリームの終端まで読もうとします。 持続的接続の場合、ストリームの終端が来るのはコネクションが閉じられるときであり、サーバがいつ閉じるかに依存します。 Content-Length で読み取るバイト数を明示するのが安全な方法です。

3 章まずは 1 リクエストだけ処理するサーバを作る)で Content-Length に基づくボディの正確な受信がいかに重要かを体験しました。 WSGI では、ソケットからの受信はサーバが担当しますが、wsgi.input からの読み取りバイト数の管理はアプリケーション側の責任です。 フレームワークがこの管理を代行してくれるので、通常は意識する必要がありませんが、仕組みとして知っておくことはトラブルシューティングの場面で役立ちます。

4.5.6. SERVER_NAME, SERVER_PORT

server_name = environ["SERVER_NAME"]  # "127.0.0.1" や "example.com"
server_port = environ["SERVER_PORT"]  # "8000" や "80"

サーバのホスト名とポート番号が文字列として格納されています。 SERVER_PORT が文字列(数値ではない)である点は、CGI の仕様に由来する歴史的な事情です。

これらの値は、アプリケーションが自分自身の URL を構築するとき(リダイレクト先の URL を生成する場合など)に使われます。 Django の request.build_absolute_uri() は、内部でこれらの値を参照して完全な URL を組み立てています。

Tip

HTTP_HOST ヘッダーと SERVER_NAME の関係は、紛らわしい点のひとつです。

キー

意味

HTTP_HOST

クライアントが送ってきた Host ヘッダーの値

SERVER_NAME

サーバの設定に基づく値

リバースプロキシを経由している場合、この2つが異なることがあります。 Django の ALLOWED_HOSTS が検証するのは HTTP_HOST の値であり、SERVER_NAME ではありません。

4.5.7. HTTP_ プレフィックス

クライアントが送ってきた HTTP ヘッダーは、特定の命名規則に従って environ に格納されます。

ヘッダーのフィールド名を大文字に変換し、ハイフンをアンダースコアに置き換え、先頭に HTTP_ を付ける。

Host: example.com          → HTTP_HOST = "example.com"
User-Agent: curl/8.7.1     → HTTP_USER_AGENT = "curl/8.7.1"
Accept: text/html           → HTTP_ACCEPT = "text/html"
Accept-Language: ja         → HTTP_ACCEPT_LANGUAGE = "ja"
X-Forwarded-For: 1.2.3.4   → HTTP_X_FORWARDED_FOR = "1.2.3.4"

3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバでは parse_headers() でヘッダーを辞書に変換し、キーを小文字に統一していました。 WSGI では逆に大文字に統一し、HTTP_ プレフィックスを付けます。この変換規則は CGI から受け継いだものです。

注意

2つのヘッダーだけはこの規則の例外です。

ヘッダー

environ のキー

備考

Content-Type

CONTENT_TYPE

HTTP_ プレフィックスなし

Content-Length

CONTENT_LENGTH

HTTP_ プレフィックスなし

これも CGI の仕様に由来する歴史的な慣例です。 Content-TypeContent-Length はリクエストの処理に不可欠なヘッダーであるため、CGI の時代から特別扱いされていました。

# ヘッダーの取得例
host = environ.get("HTTP_HOST", "")
user_agent = environ.get("HTTP_USER_AGENT", "")
content_type = environ.get("CONTENT_TYPE", "")       # HTTP_ なし
content_length = environ.get("CONTENT_LENGTH", "0")   # HTTP_ なし

Django が request.headers["Host"] のような自然な形式でヘッダーにアクセスできるのは、フレームワークが environHTTP_ プレフィックス付きのキーを逆変換しているからです。 この変換の存在を知っていると、「environ の中では HTTP_HOST だが、Django の request.META でも HTTP_HOST で、request.headers では Host になる」という対応関係が理解できます。


本項では、environ 辞書に含まれる主要なキーを確認しました。

  • REQUEST_METHOD にメソッド

  • PATH_INFO にパス

  • QUERY_STRING にクエリ文字列

  • wsgi.input にボディのストリーム

  • HTTP_* にクライアントのヘッダー

3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバで苦労して取り出していた情報が、WSGI ではすべて構造化された形で手に入ります。

次項では、environ と対になるもうひとつの重要な要素——start_response とイテラブルによるレスポンスの返却方法を詳しく見ていきます。

4.6. start_response の役割

前項で、environ 辞書を通じてリクエスト情報を受け取る方法を確認しました。 本項では、レスポンスの前半部分——ステータスコードとヘッダー——をサーバに伝えるための仕組みである start_response を掘り下げます。

WSGI のレスポンスは、2つの経路に分かれて返されます。 ステータスとヘッダーは start_response を通じて、ボディはアプリケーション callable の戻り値として。 この分離には設計上の意図があり、それを理解することが WSGI の仕組みを正しく把握する鍵になります。

4.6.1. ステータス

start_response の第1引数は、ステータスコードと理由フレーズを連結した文字列です。

start_response("200 OK", [("Content-Type", "text/plain")])

"200 OK" という文字列は、3 章まずは 1 リクエストだけ処理するサーバを作る)で自作サーバの build_status_line() が組み立てていたステータスラインの一部に対応しています。 自作サーバでは f"HTTP/1.1 {status_code} {reason}\r\n" のように HTTP バージョンや改行を含む完全なステータスラインを作っていましたが、WSGI ではステータスコードと理由フレーズだけを渡します。 HTTP バージョンの付加や改行の処理はサーバの仕事であり、アプリケーションは関与しません。

ステータス文字列の形式は「3桁の数字、半角スペース、理由フレーズ」です。

# 正しい形式
start_response("200 OK", headers)
start_response("404 Not Found", headers)
start_response("500 Internal Server Error", headers)

# 不正な形式(サーバによってはエラーになる)
start_response("200", headers)          # 理由フレーズがない
start_response(200, headers)             # 文字列ではなく整数
start_response("OK 200", headers)        # 順序が逆

仕様上、理由フレーズは空であっても構いませんが("200 " のようにスペースだけ残す形式)、慣習的な理由フレーズを含めるのが一般的です。 Django の WSGIHandler"200 OK""404 Not Found" のように標準的な理由フレーズを返します。

3 章まずは 1 リクエストだけ処理するサーバを作る)では REASON_PHRASES 辞書を作ってステータスコードと理由フレーズの対応を管理していました。 フレームワークはこの対応を内部に持っており、HttpResponse(status=404) のように数値でステータスコードを指定するだけで、適切な理由フレーズを自動的に付けてくれます。

4.6.2. ヘッダー

start_response の第2引数は、レスポンスヘッダーのリストです。各要素は (名前, 値) のタプルです。

headers = [
    ("Content-Type", "text/html; charset=utf-8"),
    ("Content-Length", "42"),
    ("X-Custom-Header", "some-value"),
]
start_response("200 OK", headers)

辞書ではなくタプルのリストを使う理由は、HTTP では同じ名前のヘッダーが複数回出現することがあり、辞書ではそれを表現できないためです。

headers = [
    ("Content-Type", "text/html; charset=utf-8"),
    ("Set-Cookie", "session_id=abc123; Path=/"),
    ("Set-Cookie", "theme=dark; Path=/"),  # 同じ名前で2つ
]

ヘッダーの名前と値はどちらも文字列でなければなりません。 バイト列ではなく、Python の str 型です。値に含まれる文字は、Latin-1(ISO-8859-1)でエンコード可能な範囲に制限されています。 これは HTTP ヘッダーの仕様に基づく制約です。

注意

WSGI の仕様では、アプリケーションが返すヘッダーにいくつかの制約があります。

アプリケーションは hop-by-hop ヘッダー(Transfer-EncodingConnection など)を返してはなりません。 これらはサーバが管理するヘッダーであり、アプリケーションが設定するとサーバの動作と矛盾する可能性があるためです。 3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバでは Connection: close を自分で設定していましたが、WSGI の世界では接続の管理はサーバの責任です。

Content-Length については少し微妙な立場にあります。 アプリケーションが Content-Length を設定することは許されていますし、設定すべきとされています。 しかし、サーバがイテラブルの内容からボディの長さを計算して自動的に付加する場合もあります。 動作はサーバの実装に依存しますが、アプリケーション側で正確な Content-Length を設定しておくのが最も確実です。

4.6.3. ボディ本体との分離

ここで立ち止まって、「なぜステータスとヘッダーを戻り値ではなく start_response というコールバックで返す設計にしたのか」を考えてみましょう。

diagram

次のような設計のほうが、一見シンプルに思えるかもしれません。

# こういう設計ではダメだったのか?
def application(environ):
    return ("200 OK", [("Content-Type", "text/plain")], [b"Hello"])

ステータス、ヘッダー、ボディをタプルにまとめて返す。 この設計でも動くことは動きます。 しかし、WSGI が start_response をコールバックにした理由は、レスポンスボディの生成が始まる前にステータスとヘッダーを確定させるという HTTP の構造を反映するためです。

重要

HTTP レスポンスは、ステータスライン → ヘッダー → 空行 → ボディ、という順序で送信されます。 ボディの最初の1バイトを送る前に、ステータスとヘッダーがすべて確定している必要があります。 start_response をコールバックにすることで、アプリケーションは「まずステータスとヘッダーを通知し、その後にボディを生成する」という HTTP の順序を自然に表現できます。

この設計が真価を発揮するのは、レスポンスボディをストリーミングで返す場合です。

def application(environ, start_response):
    start_response("200 OK", [
        ("Content-Type", "text/plain"),
        ("Transfer-Encoding", "chunked"),
    ])
    # ボディを少しずつ生成して返す
    yield b"Processing...\n"
    # 時間のかかる処理
    result = do_heavy_computation()
    yield f"Result: {result}\n".encode("utf-8")
    yield b"Done.\n"

start_response はジェネレータの最初の yield より前に呼ばれています。 サーバは start_response で受け取ったステータスとヘッダーを即座にクライアントに送信し、その後ジェネレータの各 yield からボディのチャンクを受け取るたびにクライアントに送り出すことができます。 もしステータスとヘッダーが戻り値のタプルに含まれていたら、ジェネレータのすべての yield が完了するまでタプル全体を受け取れず、ストリーミングの意味がなくなってしまいます。

もうひとつ、start_response が分離されている意味が生きる場面があります。 例外処理との組み合わせです。start_response にはオプションの第3引数 exc_info があります。

def application(environ, start_response):
    try:
        # 何らかの処理
        result = process_request(environ)
        start_response("200 OK", [("Content-Type", "text/plain")])
        return [result.encode("utf-8")]
    except Exception:
        import sys
        # エラーが発生した場合、start_response を再度呼べる
        start_response(
            "500 Internal Server Error",
            [("Content-Type", "text/plain")],
            sys.exc_info(),
        )
        return [b"An error occurred."]

注釈

通常、start_response は1回のリクエスト処理で1回だけ呼ばれます。 しかし、exc_info を渡す場合に限り、2回目の呼び出しが許されます。 これは、処理の途中でエラーが発生し、すでに start_response を呼んでいた場合に、ステータスをエラー用に差し替えるための仕組みです。

ただし、ボディの送信がすでに始まっていた場合(最初の yield がサーバに渡された後)は、ステータスの差し替えはできず、例外が再送出されます。 HTTP の性質上、ステータスラインはボディより先に送信されているため、一度送ったステータスは取り消せないからです。

この仕組みは実務で直接使うことはほとんどありません。 フレームワークがエラーハンドリングを代行してくれるからです。 しかし、WSGI の設計が HTTP の構造——ステータスが先、ボディが後、一度送った先頭部分は取り消せない——を忠実に反映していることがわかります。


本項では、start_response の役割を3つの観点から確認しました。

  1. ステータス文字列によるステータスコードの通知

  2. タプルのリストによるヘッダーの設定

  3. ボディとの分離がもたらすストリーミング対応とエラーハンドリングの柔軟性

environ がリクエストの入口であるのに対し、start_response はレスポンスの出口の前半です。 次項では、出口の後半——イテラブルとしてのレスポンスボディ——を見ていきます。 アプリケーションの戻り値がなぜリストやジェネレータでなければならないのか、その設計の意図を理解しましょう。

4.7. iterable としてのレスポンス

前項で、start_response がステータスコードとヘッダーをサーバに伝える役割を担っていることを確認しました。 レスポンスの残りの部分——ボディ——は、アプリケーション callable の戻り値として返されます。

WSGI の仕様では、この戻り値はバイト列のイテラブルでなければならないと定められています。 単なるバイト列(b"Hello")ではなく、イテラブル([b"Hello"])です。 この設計には明確な理由があります。 本項では、イテラブルとしてのレスポンスを、リストとジェネレータという2つの形式で実装しながら、その意図を理解していきます。

4.7.1. list を返す

最もシンプルで一般的なのは、バイト列を要素とするリストを返す方法です。

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello, World!"]

リストの各要素はバイト列(bytes 型)でなければなりません。 文字列(str 型)を返すとエラーになります。 3 章まずは 1 リクエストだけ処理するサーバを作る)で繰り返し確認したとおり、HTTP レスポンスのボディはバイト列としてネットワーク上を流れるものであり、文字列はバイト列ではありません。

リストに複数の要素を入れることもできます。

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/html; charset=utf-8")])
    return [
        b"<html><body>",
        b"<h1>Hello</h1>",
        b"<p>World</p>",
        b"</body></html>",
    ]

サーバはリストの各要素を順番に取り出し、HTTP レスポンスのボディとしてクライアントに送信します。 クライアントから見れば、これらの要素が連結されたひとつのボディとして見えます。

警告

「なぜ単にバイト列を返すのではなく、バイト列のリストを返すのか」という疑問が浮かぶかもしれません。 return b"Hello, World!" ではダメなのでしょうか。

技術的には、バイト列もイテラブルです。 Python では bytes 型を for ループで走査すると、1バイトずつ整数として取り出されますfor chunk in b"Hello": print(chunk)72101108108111 と出力します。 つまり、サーバがバイト列をイテレートすると、1バイトずつ送信するという恐ろしく非効率な動作になってしまいます。

バイト列をリストに入れて [b"Hello, World!"] とすることで、リストの走査で得られるのはバイト列そのもの(チャンク)になり、まとまった単位で送信できます。

Content-Length の計算は、リストの全要素の合計バイト数で行います。

def application(environ, start_response):
    body_parts = [b"<html><body>", b"<h1>Hello</h1>", b"</body></html>"]
    content_length = sum(len(part) for part in body_parts)
    start_response("200 OK", [
        ("Content-Type", "text/html; charset=utf-8"),
        ("Content-Length", str(content_length)),
    ])
    return body_parts

リストを返す場合、レスポンスボディ全体がメモリ上に存在しています。 小さなレスポンスであれば問題ありませんが、数百 MB のファイルを返す場合はどうでしょうか。 ファイル全体をメモリに読み込んでリストに入れるのは現実的ではありません。 ここで、ジェネレータの出番です。

4.7.2. generator を返す

Python のジェネレータは、値を一度にすべて生成するのではなく、必要に応じてひとつずつ生成するイテラブルです。 WSGI のレスポンスとしてジェネレータを返すと、サーバは yield されたチャンクを受け取るたびにクライアントに送信できます。

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])

    def generate():
        yield b"Line 1\n"
        yield b"Line 2\n"
        yield b"Line 3\n"

    return generate()

この例では、generate() がジェネレータ関数であり、generate() の呼び出しがジェネレータオブジェクト(イテラブル)を返します。 サーバは for chunk in result: のようにジェネレータを走査し、各チャンクをクライアントに送信します。

ジェネレータの真価は、大きなデータを扱うときに発揮されます。 たとえば、大きなファイルを返す場合を考えてみましょう。

def application(environ, start_response):
    file_path = "/path/to/large_file.csv"
    start_response("200 OK", [
        ("Content-Type", "text/csv"),
    ])

    def read_file():
        with open(file_path, "rb") as f:
            while True:
                chunk = f.read(8192)  # 8KB ずつ読む
                if not chunk:
                    break
                yield chunk

    return read_file()

ファイル全体をメモリに読み込むのではなく、8KB ずつ読み取って yield しています。 100MB のファイルであっても、メモリ上には常に最大 8KB のチャンクしか存在しません。 サーバは yield されるたびにそのチャンクをクライアントに送り出し、次の yield を待ちます。

リストで同じことをしようとすると、こうなります。

# メモリに全体を載せてしまう — 大きなファイルでは危険
def application(environ, start_response):
    with open("/path/to/large_file.csv", "rb") as f:
        body = f.read()  # ファイル全体をメモリに読み込む
    start_response("200 OK", [
        ("Content-Type", "text/csv"),
        ("Content-Length", str(len(body))),
    ])
    return [body]

動作としては正しいですが、ファイルサイズが大きくなるとメモリを圧迫します。 複数のリクエストが同時に大きなファイルをダウンロードしている状況では、サーバのメモリが枯渇する危険があります。

注釈

WSGI の仕様では、イテラブルが close() メソッドを持っている場合、サーバはイテレーションの完了後に close() を呼ばなければならないと定められています。 ジェネレータは close() メソッドを持っているので、サーバはジェネレータの走査が終わった後に自動的にクリーンアップを行います。 ファイルを開いたジェネレータの場合、with 文の中で yield しているため、ジェネレータの close() 呼び出しによって with ブロックから抜け、ファイルが確実に閉じられます。

4.7.3. ストリーミングの考え方

ジェネレータを使ったレスポンスは、ストリーミングという概念につながります。 ストリーミングとは、レスポンス全体の生成を待たずに、準備できた部分から順次クライアントに送り出す手法です。

diagram

通常のリスト返却では、アプリケーションがリスト全体を生成し終わるまで、サーバはクライアントへの送信を始められません。 一方、ジェネレータを使えば、最初の yield が実行された時点で、サーバはそのチャンクをクライアントに送信できます。

この違いが意味を持つ典型的なシナリオがあります。 データベースから大量のレコードを取得して CSV として返す API を想像してください。

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/csv")])

    def generate_csv():
        yield b"id,name,email\n"
        for user in fetch_users_from_db():  # 10万件のユーザー
            yield f"{user.id},{user.name},{user.email}\n".encode("utf-8")

    return generate_csv()
  • リストで返す場合: 10万件すべてのレコードを取得して CSV 文字列を生成し終えるまで、クライアントは何も受け取れない

  • ジェネレータで返す場合: 最初のレコードが取得できた時点でクライアントへの送信が始まる

ユーザーはダウンロードの進捗を確認でき、サーバのメモリ消費も抑えられます。

Tip

ストリーミングにはトレードオフがあります。レスポンス全体のサイズが事前にわからないため、Content-Length ヘッダーを設定できません。 この場面で使われるのが Transfer-Encoding: chunked です。 サーバはストリーミングレスポンスを chunked encoding で送信し、クライアントはチャンクごとにデータを受け取ります。

Django の StreamingHttpResponse と FastAPI の StreamingResponse は、まさにこの仕組みを利用しています。

# Django
from django.http import StreamingHttpResponse

def csv_view(request):
    def generate():
        yield "id,name,email\n"
        for user in User.objects.iterator():
            yield f"{user.id},{user.name},{user.email}\n"
    return StreamingHttpResponse(generate(), content_type="text/csv")

Django の StreamingHttpResponse は、渡されたイテラブル(ジェネレータ)を WSGI のイテラブルレスポンスとしてそのまま返します。 フレームワークが提供するストリーミング機能の裏側には、WSGI のイテラブル設計があるのです。


本項では、WSGI レスポンスのボディがイテラブルとして設計されている理由を、リストとジェネレータという2つの実装形式を通じて確認しました。

形式

特徴

向いているケース

リスト

シンプルで扱いやすい

小さなレスポンス、全体が確定している場合

ジェネレータ

メモリ効率が良い、ストリーミング可能

大きなファイル、DB の大量レコード

WSGI が戻り値を「バイト列」ではなく「バイト列のイテラブル」と定めたのは、この柔軟性を確保するためです。

ここまでで、WSGI の3つの柱——environ(リクエスト情報)、start_response(ステータスとヘッダー)、イテラブル(レスポンスボディ)——をすべて見てきました。 次項では、これらの知識を組み合わせて、完全な WSGI アプリケーションを自分の手で書いてみましょう。

4.8. 最小の WSGI アプリを書く

ここまでの3つの項で、WSGI の構成要素——environstart_response、イテラブルレスポンス——をひとつずつ確認してきました。 本項では、これらを組み合わせて実際に動く WSGI アプリケーションを書きます。 3 章まずは 1 リクエストだけ処理するサーバを作る)で自作した HTTP サーバと対比しながら進めます。 あのとき苦労して書いたソケット操作やバイト列のパースが、WSGI によってどれだけ消えるのかを体感してください。

4.8.1. Hello World

まずは最小限の WSGI アプリケーションです。

# hello_wsgi.py
def application(environ, start_response):
    body = b"Hello, WSGI World!"
    start_response("200 OK", [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ])
    return [body]

これを Python 標準ライブラリの wsgiref で動かします。

# run_hello.py
from wsgiref.simple_server import make_server
from hello_wsgi import application

server = make_server("127.0.0.1", 8000, application)
print("Serving on http://127.0.0.1:8000")
server.serve_forever()
python run_hello.py

別のターミナルから curl で確認します。

curl -v http://127.0.0.1:8000/
< HTTP/1.0 200 OK
< Content-Type: text/plain; charset=utf-8
< Content-Length: 18
<
Hello, WSGI World!

重要

3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバと比べてみましょう。

自作サーバでは次のすべてを自分で書いていました。

  1. socket.socket() の作成

  2. bind()listen()accept()

  3. recv() でのバイト列受信

  4. \r\n\r\n の検出

  5. リクエストラインのパース

  6. レスポンス文字列の組み立て

  7. sendall() での送信

  8. close() でのソケット解放

WSGI アプリケーションには、そのどれも書かれていません。ソケットの存在すら見えません。 アプリケーションの責務は「environ からリクエスト情報を受け取り、start_response でステータスとヘッダーを伝え、ボディをイテラブルで返す」この3点だけに凝縮されています。

そして、この application をそのまま Gunicorn で動かすこともできます。

pip install gunicorn
gunicorn hello_wsgi:application

アプリケーションのコードは1文字も変えていません。 サーバだけが wsgiref から Gunicorn に入れ替わりました。 「共通インタフェースによる疎結合」が、ここで実現しています。

4.8.2. ルーティング付き最小実装

Hello World だけでは物足りないので、パスとメソッドに基づくルーティングを追加しましょう。 3 章まずは 1 リクエストだけ処理するサーバを作る)で if ... elif ... else の連鎖で書いたルーティングを、WSGI アプリケーションとして書き直します。

# routing_wsgi.py
from urllib.parse import parse_qs


def application(environ, start_response):
    method = environ["REQUEST_METHOD"]
    path = environ.get("PATH_INFO", "/")
    query_string = environ.get("QUERY_STRING", "")

    if path == "/" and method == "GET":
        body = b"Welcome to the top page!"
        start_response("200 OK", [
            ("Content-Type", "text/plain; charset=utf-8"),
            ("Content-Length", str(len(body))),
        ])
        return [body]

    elif path == "/greet" and method == "GET":
        params = parse_qs(query_string)
        name = params.get("name", ["World"])[0]
        body = f"Hello, {name}!".encode("utf-8")
        start_response("200 OK", [
            ("Content-Type", "text/plain; charset=utf-8"),
            ("Content-Length", str(len(body))),
        ])
        return [body]

    elif path == "/about" and method == "GET":
        body = b"This is a WSGI application."
        start_response("200 OK", [
            ("Content-Type", "text/plain; charset=utf-8"),
            ("Content-Length", str(len(body))),
        ])
        return [body]

    else:
        body = f"Not Found: {path}".encode("utf-8")
        start_response("404 Not Found", [
            ("Content-Type", "text/plain; charset=utf-8"),
            ("Content-Length", str(len(body))),
        ])
        return [body]
curl http://127.0.0.1:8000/
# → Welcome to the top page!

curl "http://127.0.0.1:8000/greet?name=Taro"
# → Hello, Taro!

curl http://127.0.0.1:8000/about
# → This is a WSGI application.

curl http://127.0.0.1:8000/nonexistent
# → Not Found: /nonexistent

3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバのルーティングと見比べてみてください。 ロジックの構造はほぼ同じです。methodpath の取得方法が変わっただけです。 自作サーバでは parse_request_line(header_part) を呼んでバイト列からメソッドとパスを取り出していましたが、WSGI では environ["REQUEST_METHOD"]environ["PATH_INFO"] から直接取り出せます。

ただし、このコードには明らかな冗長さがあります。 すべてのルートで start_response の呼び出しと Content-Length の計算が繰り返されています。 3 章まずは 1 リクエストだけ処理するサーバを作る)で作った make_response() のようなヘルパーが欲しくなります。

# routing_wsgi_v2.py
from urllib.parse import parse_qs


def make_text_response(start_response, status, text):
    body = text.encode("utf-8")
    start_response(status, [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ])
    return [body]


def application(environ, start_response):
    method = environ["REQUEST_METHOD"]
    path = environ.get("PATH_INFO", "/")
    query_string = environ.get("QUERY_STRING", "")

    if path == "/" and method == "GET":
        return make_text_response(start_response, "200 OK", "Welcome to the top page!")

    elif path == "/greet" and method == "GET":
        params = parse_qs(query_string)
        name = params.get("name", ["World"])[0]
        return make_text_response(start_response, "200 OK", f"Hello, {name}!")

    elif path == "/about" and method == "GET":
        return make_text_response(start_response, "200 OK", "This is a WSGI application.")

    else:
        return make_text_response(start_response, "404 Not Found", f"Not Found: {path}")

コラム: フレームワークへの自然な進化

make_text_response() はエンコーディングと Content-Length の計算を隠蔽し、呼び出し側をすっきりさせています。 この小さなヘルパーは、フレームワークが提供する HttpResponseResponse の最も原始的な祖先と言えるかもしれません。

コードの冗長さを解消しようとすると、自然とフレームワークの方向に向かっていくのです。

4.8.3. JSON を返す例

REST API では JSON レスポンスが標準的です。 WSGI アプリケーションで JSON を返す例を書いてみましょう。

# json_wsgi.py
import json
from urllib.parse import parse_qs


USERS = {
    1: {"id": 1, "name": "Taro Yamada", "email": "[email protected]"},
    2: {"id": 2, "name": "Hanako Sato", "email": "[email protected]"},
    3: {"id": 3, "name": "Jiro Suzuki", "email": "[email protected]"},
}


def json_response(start_response, status, data):
    body = json.dumps(data, ensure_ascii=False).encode("utf-8")
    start_response(status, [
        ("Content-Type", "application/json; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ])
    return [body]


def application(environ, start_response):
    method = environ["REQUEST_METHOD"]
    path = environ.get("PATH_INFO", "/")

    if path == "/users" and method == "GET":
        user_list = list(USERS.values())
        return json_response(start_response, "200 OK", user_list)

    elif path.startswith("/users/") and method == "GET":
        try:
            user_id = int(path.split("/")[2])
        except (IndexError, ValueError):
            return json_response(
                start_response,
                "400 Bad Request",
                {"error": "Invalid user ID"},
            )
        user = USERS.get(user_id)
        if user is None:
            return json_response(
                start_response,
                "404 Not Found",
                {"error": f"User {user_id} not found"},
            )
        return json_response(start_response, "200 OK", user)

    elif path == "/users" and method == "POST":
        content_length = int(environ.get("CONTENT_LENGTH", "0") or "0")
        body = environ["wsgi.input"].read(content_length)
        try:
            data = json.loads(body.decode("utf-8"))
        except (json.JSONDecodeError, UnicodeDecodeError):
            return json_response(
                start_response,
                "400 Bad Request",
                {"error": "Invalid JSON"},
            )
        new_id = max(USERS.keys()) + 1
        new_user = {"id": new_id, "name": data.get("name", ""), "email": data.get("email", "")}
        USERS[new_id] = new_user
        return json_response(start_response, "201 Created", new_user)

    else:
        return json_response(
            start_response,
            "404 Not Found",
            {"error": f"Not Found: {path}"},
        )

curl で動作を確認します。

curl http://127.0.0.1:8000/users
# → [{"id": 1, "name": "Taro Yamada", ...}, ...]

curl http://127.0.0.1:8000/users/2
# → {"id": 2, "name": "Hanako Sato", "email": "[email protected]"}

curl http://127.0.0.1:8000/users/99
# → {"error": "User 99 not found"}

curl -X POST http://127.0.0.1:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Yuki Tanaka", "email": "[email protected]"}'
# → {"id": 4, "name": "Yuki Tanaka", "email": "[email protected]"}

3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバで書いた JSON API と比較してみてください。 ルーティングやボディのパースといったアプリケーションロジックはほぼ同じですが、ソケット操作が完全に消えています。

  • recv() の代わりに environ["wsgi.input"].read() がある

  • sendall() の代わりにイテラブルの return がある

POST のボディ読み取りでは、CONTENT_LENGTHwsgi.input を使っています。 environ.get("CONTENT_LENGTH", "0") or "0" という書き方は少し冗長ですが、CONTENT_LENGTH が空文字列の場合に対応するためのものです。 こうした細かな防御的コードが、フレームワークを使えば request.json() の一言で済むことを思うと、フレームワークのありがたみを改めて感じます。

注釈

このコードのルーティング部分を見ると、path.startswith("/users/") でパスの先頭一致を判定し、path.split("/")[2] でパスパラメータを取り出すという素朴な実装になっています。

  • /users/42/profile のようなネストしたパスには対応できない

  • パスパラメータの型変換も自分で書く必要がある

Django の <int:user_id> や FastAPI の {user_id}: int が、こうした面倒をどれほど引き受けてくれているかが実感できるのではないでしょうか。


本項では、WSGI アプリケーションを Hello World から始め、ルーティング、クエリパラメータ、JSON レスポンス、POST ボディの受信まで実装しました。 3 章まずは 1 リクエストだけ処理するサーバを作る)の自作サーバとの対比で、WSGI がサーバの責務(ソケット操作、HTTP パース)をアプリケーションから切り離してくれていることを確認し、同時にルーティングの冗長さやヘルパー関数への欲求を通じて、WSGI の上にフレームワークが欲しくなる感覚も体験しました。

次項では、WSGI のもうひとつの重要な概念——ミドルウェアについて見ていきます。 サーバとアプリケーションの間に「もう一枚の層」を挟むことで、横断的な関心事をどう扱うかを学びます。

4.9. WSGI の良さと限界

4 章WSGI が生まれた背景)を通じて、WSGI の背景、仕様、そして実装を見てきました。 environstart_response というシンプルなインタフェースが、Python の Web エコシステムを支えてきたことを実感できたのではないでしょうか。

本項では、WSGI を一歩引いた視点から評価します。 WSGI の何が優れていて、どこに限界があるのか。 この整理は、Vol.2「なぜ ASGI が必要になったのか」で出会う ASGI がなぜ必要とされたのかを理解するための布石になります。

4.9.1. シンプルさ

WSGI の最大の美点は、仕様の徹底的なシンプルさです。

アプリケーション側の要件を思い出してください。 「2つの引数を受け取る callable であり、start_response を呼んでからバイト列のイテラブルを返す」。 これがすべてです。 特別な基底クラスを継承する必要もなければ、特定のデコレータを適用する必要もありません。 Python の関数をひとつ書くだけで、WSGI アプリケーションが完成します。

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello"]

この3行に、WSGI の仕様が要求するすべてが含まれています。 PEP 3333 の仕様書自体も、他のプロトコル仕様と比べると驚くほど短い文書です。

シンプルさは次のような恩恵をもたらします。

  • 学習の容易さ: environstart_response、イテラブルという3つの概念を学ぶだけで全体像を把握できる

  • 実装の容易さ: WSGI サーバ、アプリケーション、ミドルウェアのどれを書くにも、特別に複雑なコードは不要

注釈

このシンプルさは偶然の産物ではなく、意図的な設計判断です。 PEP 333 の著者 Phillip Eby は、WSGI を「フレームワークの著者が使うための低レベルのインタフェース」と位置づけ、あえて高レベルの機能(セッション管理やテンプレートエンジンなど)を仕様に含めませんでした。 最小限のインタフェースだけを定義し、それ以上のことはフレームワークに委ねる。 この割り切りが、WSGI の20年以上にわたる長寿命につながっています。

4.9.2. 相互運用性

WSGI のもうひとつの大きな功績は、Python Web エコシステムに相互運用性をもたらしたことです。

WSGI 以前の Python Web 開発は、サーバとフレームワークの組み合わせが固定されがちでした。 WSGI の登場によって、この制約は解消されました。

  • Django は Gunicorn でも uWSGI でも mod_wsgi でも動く

  • Flask も Bottle も Pyramid も同様

  • Gunicorn は Django でも Flask でもどんな WSGI アプリケーションでも動かせる

相互運用性は、ミドルウェアの領域でも発揮されます。 WSGI ミドルウェアは、サーバから見ればアプリケーション、アプリケーションから見ればサーバとして振る舞います。 このため、任意のフレームワークと組み合わせて使えるミドルウェアを書くことができます。 認証、ログ記録、エラー報告、プロファイリング——これらの横断的な処理を、フレームワーク非依存の形で提供できるのです。

Tip

本番環境でサーバを切り替えるという判断は、実務では珍しくありません。

「開発環境では wsgiref で手軽に起動し、ステージング環境では Gunicorn で動作確認し、本番環境では uWSGI を使う」——こうした運用が可能なのは、WSGI のおかげです。 アプリケーションのコードは一切変更する必要がありません。

4.9.3. 同期前提

WSGI の限界は、その設計の前提にあります。 WSGI は同期処理を前提として設計されています。

diagram

WSGI アプリケーションの callable は、呼び出されたら処理が完了するまで戻らないことが期待されています。 application(environ, start_response) が呼ばれ、内部でデータベースに問い合わせ、結果を加工し、レスポンスを組み立て、イテラブルを返す。 この一連の処理はすべて同期的に、つまり順番に実行されます。

データベースの問い合わせに100ミリ秒、外部APIの呼び出しに200ミリ秒かかるとしましょう。 WSGI の同期モデルでは、ひとつのリクエストを処理している間、その処理を担当しているワーカー(プロセスまたはスレッド)は他のことができません。 データベースの応答を待っている100ミリ秒間も、外部APIの応答を待っている200ミリ秒間も、ワーカーはただ待っているだけです。

Gunicorn のプリフォークモデルでは、この問題を複数のワーカープロセスで緩和します。 4つのワーカーを起動すれば、同時に4つのリクエストを処理できます。 しかし、プロセスはメモリを消費するリソースです。 数百、数千の同時接続を捌くために数百のプロセスを立ち上げるのは現実的ではありません。

Python の asyncio は、この「I/O 待ちの間に他の処理を進める」という問題を、非同期プログラミングによって解決します。 ひとつのプロセス、ひとつのスレッドの中で、複数のリクエストの I/O 待ちを効率的に切り替えます。 しかし、WSGI のインタフェースは async def の callable を想定していません。

危険

次のコードは WSGI として動作しません。

# これは WSGI として動かない
async def application(environ, start_response):
    data = await fetch_from_database()  # WSGI サーバは await を理解しない
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [data.encode("utf-8")]

environstart_response を受け取って同期的にイテラブルを返す——この設計の中に、await を挟み込む余地がないのです。 WSGI サーバは application(environ, start_response) を通常の関数呼び出しとして実行します。 戻り値がコルーチンオブジェクトだった場合、サーバはそれをイテラブルとして走査しようとしますが、期待どおりには動きません。 WSGI の仕様と asyncio の実行モデルは根本的に異なります。



### WebSocket 非対応

WSGI のもうひとつの限界は、**WebSocket をはじめとする双方向通信に対応できない**ことです。

WSGI のモデルは「1リクエストに対して1レスポンスを返す」という HTTP/1.1 の基本的なやりとりを前提にしています。
サーバがアプリケーションを呼び出し、アプリケーションがレスポンスを返し、それで1回のやりとりが完結します。
このモデルは HTTP のリクエスト/レスポンスパターンに完全に対応していますが、WebSocket には対応できません。

WebSocket は、HTTP のアップグレード機構を使って確立される双方向の永続的な通信チャネルです。
クライアントとサーバが対等にメッセージを送り合い、接続はどちらかが明示的に閉じるまで維持されます。
チャットアプリケーション、リアルタイム通知、ライブフィードなど、サーバからクライアントに能動的にデータを送りたい場面で使われます。

WSGI のインタフェースでは、「サーバからクライアントに能動的にメッセージを送る」という動作を表現する方法がありません。

- `application(environ, start_response)` は呼び出しから戻り値の返却までが1回のやりとりであり、戻った後にアプリケーションからサーバに追加のデータを送る手段がない
- クライアントからの追加のメッセージを受け取る手段もない
- `environ["wsgi.input"]` はリクエストボディを読み取るためのものであり、WebSocket のメッセージストリームを扱うようには設計されていない

HTTP/2 のサーバプッシュや Server-Sent Events(SSE)も、WSGI のモデルでは自然に表現できません。
SSE はジェネレータを使ったストリーミングレスポンスで近似的に実装できるケースもありますが、それは WSGI の設計意図を超えた使い方であり、すべてのサーバで正しく動作する保証はありません。

---

```{important}
WSGI の評価をまとめると、次のようになります。

| 観点 | 評価 |
|------|------|
| シンプルさ | 非常に優れている。3行で最小アプリが書ける |
| 相互運用性 | 非常に優れている。サーバとフレームワークを自由に組み合わせられる |
| 同期 I/O の効率 | 限界あり。asyncio との組み合わせが不可能 |
| WebSocket 対応 | 不可。双方向通信を表現できない仕様 |

同期的なリクエスト/レスポンスのパターンで完結するアプリケーションにとって、WSGI は今なお十分な仕様です。
Django も Flask も、今日でも WSGI の上で動いています。

しかし、Web の世界は変化しています。 非同期 I/O による効率的な並行処理、WebSocket によるリアルタイム通信——これらの需要に対して、WSGI は構造的に対応できません。 この限界を超えるために生まれたのが、Vol.2「なぜ ASGI が必要になったのか」で学ぶ ASGI(Asynchronous Server Gateway Interface)です。 ASGI は WSGI の精神——サーバとアプリケーションの間の共通インタフェース——を受け継ぎつつ、非同期処理と双方向通信を仕様の中に取り込んでいます。

その前に、次の5 章WSGI の上に何が必要になるのか)では WSGI の上に構築されたフレームワークの内側を見ていきます。 Werkzeug、Bottle、そして Flask が、WSGI の environstart_response をどのように包み込んで開発者に優しいインタフェースを提供しているのかを確認しましょう。

4.10. トラブルシューティングの観点

4 章WSGI が生まれた背景)の締めくくりとして、WSGI の知識が実際のトラブルシューティングでどう役立つかを具体的に見ていきます。

フレームワークを使った日常の開発では、environstart_response を直接触ることはほとんどありません。 しかし、フレームワークの動作が期待どおりにならないとき、一段下の WSGI レイヤーに降りて確認することで、問題の原因が見えることがあります。 3 章まずは 1 リクエストだけ処理するサーバを作る)のトラブルシューティングの項で「生の HTTP を見る」ことの価値を議論しましたが、WSGI はその HTTP とフレームワークの間にある層です。 HTTP の生のバイト列よりは扱いやすく、フレームワークの抽象よりは生々しい——ちょうど良い観察ポイントなのです。

4.10.1. environ をダンプする

「リクエストの情報がフレームワークに正しく届いているかわからない」——こうした状況で最も直接的な手段は、environ の中身をそのまま出力することです。

environ のダンプアプリケーションを書きましたが、フレームワークのビュー関数の中からでも同じことができます。 Django であれば request.METAenviron をほぼそのまま保持しています。

# Django のビュー関数で environ を確認する
def debug_view(request):
    from django.http import JsonResponse
    meta = {k: str(v) for k, v in request.META.items() if isinstance(v, str)}
    return JsonResponse(meta, json_dumps_params={"indent": 2})

FastAPI でも、Starlette の Request オブジェクトを通じてスコープ情報にアクセスできます(FastAPI は ASGI ベースなので厳密には environ ではなく scope ですが、考え方は同じです)。

しかし、フレームワークを経由せずに environ を直接見たい場面もあります。 フレームワーク自体の初期化で問題が起きている場合や、ミドルウェアがリクエスト情報を書き換えているかもしれない場合です。 こうしたときは、フレームワークの手前に簡易的な WSGI ミドルウェアを挿入して environ を覗きます。

# environ_debug_middleware.py
import sys


class EnvironDebugMiddleware:
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        print("=== environ dump ===", file=sys.stderr)
        for key in sorted(environ):
            value = environ[key]
            if isinstance(value, str):
                print(f"  {key}: {value!r}", file=sys.stderr)
        print("=== end ===", file=sys.stderr)
        return self.app(environ, start_response)

このミドルウェアを本来のアプリケーションの前段に挟むだけで、すべてのリクエストの environ が標準エラー出力にダンプされます。 問題の調査が終わったら外す、という使い捨ての道具です。

# Gunicorn で使う場合
from myapp.wsgi import application
from environ_debug_middleware import EnvironDebugMiddleware

application = EnvironDebugMiddleware(application)

Tip

environ をダンプすると、次のような問題をたちどころに発見できます。

  • リバースプロキシの設定ミスで HTTP_HOST の値がおかしくなっている

  • SCRIPT_NAME が意図しない値になっている

  • HTTPS 環境なのに wsgi.url_schemehttp のままになっている

4.10.2. ヘッダーがどこに入るか

「クライアントが送ったはずのカスタムヘッダーが、フレームワーク側で取得できない」という問題は、WSGI のヘッダー変換規則を知らないと混乱しがちです。

HTTP ヘッダーは environ に格納される際に変換を受けます。 大文字に変換し、ハイフンをアンダースコアに置き換え、HTTP_ プレフィックスを付ける。 たとえば、クライアントが X-Request-Id: abc123 というカスタムヘッダーを送った場合、environ では HTTP_X_REQUEST_ID というキーに格納されます。

curl -H "X-Request-Id: abc123" http://127.0.0.1:8000/
# environ 内では
environ["HTTP_X_REQUEST_ID"]  # "abc123"

Django の request.META もこの形式を踏襲しています。 request.META["HTTP_X_REQUEST_ID"] でアクセスします。 Django 2.2 以降の request.headers は逆変換を行い、request.headers["X-Request-Id"] という自然な形式でアクセスできるようにしていますが、内部的には environ のキーを変換しているだけです。

注意

この変換規則を知っていると、次のような問題を瞬時に診断できます。

よくある間違い: 「Authorization ヘッダーを送っているのに、Django の request.META で取得できない」

request.META["Authorization"] で探していませんか。正しくは request.META["HTTP_AUTHORIZATION"] です。

もうひとつの落とし穴: Content-TypeContent-Length が例外的に HTTP_ プレフィックスなしで格納されます。

  • environ["HTTP_CONTENT_TYPE"] は存在しません → environ["CONTENT_TYPE"] が正しい

  • environ["HTTP_CONTENT_LENGTH"] は存在しません → environ["CONTENT_LENGTH"] が正しい

もうひとつ見落としやすいのは、ヘッダーのフィールド名にアンダースコアが含まれている場合です。 たとえば X_Custom_Header というヘッダー名は、WSGI の変換後に HTTP_X_CUSTOM_HEADER になります。 ところが、X-Custom-Header(ハイフン区切り)も変換後は同じ HTTP_X_CUSTOM_HEADER になります。 この2つが区別できなくなるのです。 Nginx はデフォルトでアンダースコアを含むヘッダーを無視する設定になっている(underscores_in_headers off)ため、リバースプロキシ経由の環境ではそもそもアンダースコア入りのヘッダーが届かないこともあります。

4.10.3. body 読み取りミス

POST リクエストのボディを扱うとき、wsgi.input からの読み取りに関連する問題がいくつかあります。

警告

最もよくある問題は、ボディを2回読もうとすることです。

wsgi.input はファイルライクオブジェクトであり、.read() を呼ぶとストリームの現在位置が進みます。 一度読んだ後にもう一度 .read() を呼んでも、空のバイト列が返ります。

def application(environ, start_response):
    content_length = int(environ.get("CONTENT_LENGTH", "0") or "0")

    # 1回目の読み取り — 成功
    body1 = environ["wsgi.input"].read(content_length)

    # 2回目の読み取り — 空になる
    body2 = environ["wsgi.input"].read(content_length)
    # body2 は b"" になる

フレームワークを使っている場合、この問題はより見つけにくい形で現れます。 たとえば、WSGI ミドルウェアがログのためにボディを読み取り、そのあとフレームワークがボディを読もうとしても空になっている、というケースです。

ミドルウェアがボディを読んだ後に wsgi.input を巻き戻す(seek(0) する)か、読み取った内容を別の場所に保存しておく必要があります。 ただし、すべての wsgi.input オブジェクトが seek() をサポートしているわけではないため、この対処が使えるかどうかはサーバの実装に依存します。


もうひとつの問題は、`CONTENT_LENGTH` の取り扱いです。
`CONTENT_LENGTH` は存在しないか空文字列の場合があります。

```python
# 危険な書き方
content_length = int(environ["CONTENT_LENGTH"])  # KeyError または ValueError

# 安全な書き方
content_length = int(environ.get("CONTENT_LENGTH", "0") or "0")

environ.get("CONTENT_LENGTH", "0") だけでは不十分です。CONTENT_LENGTH がキーとして存在するが値が空文字列("")という場合があり、int("")ValueError を投げます。or "0" を追加することで、空文字列を "0" にフォールバックさせています。

この防御的な書き方が冗長に感じるのは当然です。 Django は request.body でこの面倒を完全に隠してくれますし、FastAPI は型ヒントと Pydantic でさらに手厚く処理してくれます。 しかし、フレームワークの奥で何が起きているかを知っているからこそ、「request.body が空になる」という問題に遭遇したとき、CONTENT_LENGTHwsgi.input のレイヤーまで降りて原因を追えるのです。

4.10.4. iterable を返す意味の誤解

WSGI アプリケーションの戻り値に関して、いくつかの典型的な間違いがあります。

注意

間違い1: バイト列をリストに入れずに直接返す

# 間違い — バイト列を直接返している
def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return b"Hello, World!"

bytes 型はイテラブルですが、走査すると1バイトずつ整数が返されます。 サーバが for chunk in result: で走査すると、各チャンクが整数(72101108…)になり、バイト列として送信できずにエラーになります。 正しくは return [b"Hello, World!"] です。

間違い2: 文字列のリストを返す

# 間違い — 文字列を返している
def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return ["Hello, World!"]

WSGI の仕様では、イテラブルの各要素は bytes 型でなければなりません。 str 型を返すと、サーバによってはエラーになります。 正しくは return [b"Hello, World!"] です。


もうひとつ見落としやすいのは、**ジェネレータの `close()` が持つ意味**です。
WSGI サーバはイテラブルの走査が完了した後、イテラブルが `close()` メソッドを持っていればそれを呼びます。
ジェネレータは `close()` を持っているため、自動的にクリーンアップが行われます。

しかし、カスタムのイテラブルクラスを作る場合は、`close()` を自分で実装する必要があります。

```python
class FileResponse:
    def __init__(self, filepath):
        self.file = open(filepath, "rb")

    def __iter__(self):
        while True:
            chunk = self.file.read(8192)
            if not chunk:
                break
            yield chunk

    def close(self):
        self.file.close()  # これがないとファイルが閉じられない

危険

close() を実装し忘れると、ファイルディスクリプタがリークします。 リクエストのたびにファイルが開かれ、閉じられないまま蓄積し、最終的に OS の「開けるファイル数の上限」に達してサーバが新しいリクエストを処理できなくなります。 本番環境で実際に起こりうる問題ですので、カスタムイテラブルを作る際は必ず close() を実装してください。


本項では、WSGI のレイヤーで発生しやすいトラブルとその診断方法を確認しました。

  • environ のダンプによるリクエスト情報の確認

  • ヘッダーの変換規則(HTTP_ プレフィックスと例外)

  • wsgi.input のストリーム特性(1回しか読めない)

  • イテラブルの正しい返し方(バイト列のリスト)

いずれも、WSGI の仕様を理解していれば自然と避けられる問題であり、理解していなければ原因の特定に苦労する問題です。

4 章WSGI が生まれた背景)はここで終わりです。 WSGI が生まれた背景から仕様の詳細、最小実装、そしてトラブルシューティングまでを一気に駆け抜けました。 次の5 章WSGI の上に何が必要になるのか)では、WSGI の上に構築されたフレームワーク——Werkzeug、Bottle、Flask——の内部を覗きます。 本章で学んだ environstart_response が、フレームワークの中でどのように包み込まれ、開発者にとって使いやすいインタフェースに変換されているのかを見ていきましょう。