(WSGI が生まれた背景)= # WSGI が生まれた背景 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)では、ソケットだけで HTTP サーバを自作し、その限界を確認しました。 同時接続ができない、エラー処理が脆弱、HTTP 仕様への完全な準拠が困難——これらはすべて、サーバ部分を自分で書くことの大変さに起因する問題でした。 しかし、もうひとつ見過ごせない問題がありました。 自作サーバでは、ソケットの操作とアプリケーションのロジック(ルーティング、ビジネスロジック、レスポンス生成)が同じコードの中に混在していました。 サーバを別の実装に差し替えたければ、アプリケーション全体を書き直さなければなりません。 この密結合の問題を解決するために生まれたのが **WSGI(Web Server Gateway Interface)** です。 WSGI は 2003 年に PEP 333 として提案され、2010 年に PEP 3333 として Python 3 に対応する形で改訂されました。 本項では、WSGI がなぜ必要とされたのか、その歴史的な背景を追いかけていきます。 ## CGI の問題 WSGI の前身とも言える仕組みが **CGI(Common Gateway Interface)** です。 ```{mermaid} sequenceDiagram participant C as クライアント participant W as Web サーバ (Apache) participant P as CGI スクリプト (新プロセス) C->>W: HTTP リクエスト W->>P: プロセス起動 + 環境変数設定 P->>P: Python 起動・モジュール読み込み・処理 P->>W: 標準出力にレスポンス書き出し P->>P: プロセス終了 W->>C: HTTP レスポンス ``` CGI は 1993 年に登場した、Web サーバから外部プログラムを呼び出すための仕様です。 CGI の動作はシンプルです。 - Web サーバ(Apache など)が HTTP リクエストを受け取ると、リクエストの情報を**環境変数**に設定する - 外部プログラム(CGI スクリプト)を**新しいプロセスとして起動**する - CGI スクリプトは環境変数からリクエスト情報を読み取り、処理結果を**標準出力**に書き出す - Web サーバはその標準出力を HTTP レスポンスとしてクライアントに返す Python の CGI スクリプトは、たとえば次のような姿をしていました。 ```python #!/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"") print(f"

Method: {method}

") print(f"

Path: {path}

") print(f"

Query: {query}

") print(f"") ``` `os.environ` からリクエスト情報を取得し、`print()` でレスポンスを出力します。 自作サーバで `recv()` と `sendall()` を使って行っていたことを、環境変数と標準出力という別のインタフェースで行っている、とも言えます。 CGI は、Web サーバとアプリケーションを分離するという意味では、正しい方向への一歩でした。 ```{warning} CGI には致命的なパフォーマンス上の問題があります。**リクエストのたびに新しいプロセスが起動される**のです。 プロセスの起動は、OS にとって軽い操作ではありません。次の処理がリクエストごとに毎回実行されます。 - メモリの確保 - Python インタプリタの起動 - モジュールのインポート - スクリプトのパース 小規模なサイトではこれでも動きましたが、リクエスト数が増えるとプロセスの起動コストがボトルネックになります。 ``` mod_python(Apache に Python インタプリタを組み込むモジュール)や FastCGI(プロセスを永続化して再利用する仕組み)など、CGI のパフォーマンス問題を解決する技術も登場しました。 しかし、これらはそれぞれ独自のインタフェースを持っていたため、新たな問題を生むことになります。 ## Python Web の断片化 2000 年代初頭の Python Web 開発は、混沌とした状況にありました。 Web サーバとアプリケーションの接続方法が統一されていなかったのです。 CGI、mod_python、FastCGI、SCGI——それぞれの仕組みが独自のインタフェースを定めており、アプリケーション側はどのサーバで動かすかに応じてコードを書き分ける必要がありました。 Web フレームワークの側も同様に断片化していました。 Zope、Twisted、Quixote、CherryPy、Webware——それぞれが独自のサーバ連携の仕組みを持っていました。 あるフレームワークで書いたアプリケーションを、別のサーバの上で動かすことは簡単ではありませんでした。 この状況は、フレームワークの開発者にもアプリケーションの開発者にも不幸でした。 - **フレームワーク開発者**: 複数のサーバ環境に対応するためのアダプターを書き続けなければならない - **アプリケーション開発者**: サーバやフレームワークの選択が将来のデプロイ環境を制約することを覚悟しなければならない ```{note} PEP 333 の冒頭は、まさにこの問題を指摘しています。 「Python には Web フレームワークが多数存在するが、フレームワークの選択が使用可能な Web サーバを制限し、その逆もまた然りである」 Java の世界ではサーブレット API が、この問題を早い段階で解決していました。 Python にも同じような共通インタフェースが必要だ——これが WSGI の出発点です。 ``` ## 共通インタフェースの必要性 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で自作サーバを書いた経験を思い出してください。 `handle_request()` 関数は、ソケットから受け取ったバイト列をパースし、パスとメソッドに基づいて分岐し、レスポンスを組み立てて返していました。 サーバの仕事(ソケット操作、HTTP パース)とアプリケーションの仕事(ルーティング、ビジネスロジック)が一体化していました。 もし「自作サーバのアプリケーション部分を、Gunicorn の上でもそのまま動かしたい」と思ったら、どうすればよいでしょうか。 自作サーバは `handle_request(header_part, body)` という独自のインタフェースでアプリケーションを呼び出していますが、Gunicorn はそのインタフェースを知りません。 Gunicorn が理解できる形でアプリケーションを書き直す必要があります。 ここで、両者の間に**共通の約束事**があったらどうでしょう。 「サーバはリクエスト情報をこういう形式でアプリケーションに渡す。アプリケーションはレスポンスをこういう形式でサーバに返す。」——この約束事さえ守れば、どのサーバの上でもアプリケーションが動き、どのアプリケーションも任意のサーバの上で動きます。 ``` サーバ A ─┐ ┌─ アプリケーション X サーバ B ─┼─ [ 共通IF ] ─┼─ アプリケーション Y サーバ C ─┘ └─ アプリケーション Z ``` ```{mermaid} flowchart LR SA[サーバ A
Gunicorn] SB[サーバ B
uWSGI] SC[サーバ C
mod_wsgi] W[WSGI
共通IF] AX[アプリ X
Django] AY[アプリ Y
Flask] AZ[アプリ Z
Bottle] SA --> W SB --> W SC --> W W --> AX W --> AY W --> AZ ``` ```{tip} 共通インタフェースの効果は組み合わせ数で実感できます。 - 共通インタフェースが**ない**場合: サーバ 3 種類 × アプリ 3 種類 = 最大 **9 種類**のアダプターが必要 - 共通インタフェースが**ある**場合: 各サーバ 1 つ + 各アプリ 1 つ = 合計 **6 つ**の実装だけで全組み合わせが動く サーバやアプリケーションの数が増えるほど、この効果は大きくなります。 ``` WSGI が定めたのは、まさにこの共通インタフェースです。 そしてその設計は驚くほどシンプルです。 アプリケーション側の要件は「2つの引数を受け取る callable であること」、それだけです。 ```python def application(environ, start_response): start_response("200 OK", [("Content-Type", "text/plain")]) return [b"Hello, World!"] ``` - `environ`: リクエスト情報を格納した辞書 - `start_response`: ステータスコードとヘッダーをサーバに伝えるための callable この3行が、WSGI に準拠した最小のアプリケーションです。 ```{admonition} コラム: CGI の設計を受け継ぐ WSGI :class: tip 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 アプリケーションを書いていきましょう。 (WSGI の全体像)= ## WSGI の全体像 前項で、WSGI が生まれた背景——CGI のパフォーマンス問題、Python Web エコシステムの断片化、共通インタフェースの必要性——を確認しました。 本項では、WSGI の仕様そのものに踏み込み、サーバとアプリケーションがどのように連携するかの全体像を把握します。 WSGI の仕様(PEP 3333)は決して長い文書ではありませんが、初めて読むと抽象的に感じるかもしれません。 本項では、{numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で自作サーバを書いた経験と対比させながら、WSGI の設計を具体的に理解していきます。 ### 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(アプリケーション) ``` ```{mermaid} flowchart LR C[クライアント] N[Nginx
Webサーバ] G[Gunicorn
WSGIサーバ] D[Django
アプリケーション] C -->|HTTP| N N -->|HTTP / Unixソケット| G G -->|"environ / start_response
(WSGI)"| D ``` ```{note} WSGI が規定しているのは、Gunicorn と Django の間の「接点」の仕様だけです。 - Nginx と Gunicorn の間: HTTP(または Unix ソケット)で通信 → **WSGI の範囲外** - Gunicorn と Django の間: WSGI インタフェースで接続 → **WSGI の守備範囲** ``` 開発環境では、この構成がもっとシンプルになります。 Django の `manage.py runserver` は、簡易的な WSGI サーバと Django アプリケーションを一体化して起動します。 Nginx は介在しません。 しかし、内部的には WSGI インタフェースを通じてリクエストを処理しているため、本番環境で Gunicorn に切り替えても Django のアプリケーションコードは一切変更する必要がありません。 これが共通インタフェースの力です。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバに当てはめると、ソケット操作と HTTP パースを行う部分が「WSGI サーバ」に相当し、`handle_request()` 以降の処理が「WSGI アプリケーション」に相当します。 自作サーバではこの2つが同じファイルの中に書かれていましたが、WSGI はこの2つの間に明確な境界線を引きます。 ### アプリケーション callable という考え方 WSGI のアプリケーション側の仕様は、驚くほどシンプルです。 アプリケーションは、**2つの引数を受け取る callable** でなければならない。それだけです。 ```python 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 アプリケーションを実装しています。 ``` ```python # 関数として実装 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_METHOD`、`PATH_INFO`、`QUERY_STRING`、`HTTP_HOST` など - WSGI 独自のキー: `wsgi.input`、`wsgi.errors` など {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバで `parse_request_line()` や `parse_headers()` を使って取り出していた情報が、WSGI ではすでにパース済みの状態で `environ` 辞書に入っているのです。 `environ` の詳細は次項で掘り下げます。 **第2引数 `start_response` は callable である。** サーバがアプリケーションを呼び出すときに、`start_response` という callable を渡してきます。 アプリケーションはレスポンスボディを返す前に、この `start_response` を呼び出して、ステータスコードとレスポンスヘッダーをサーバに伝えます。 ```python start_response("200 OK", [("Content-Type", "text/plain")]) ``` - 第1引数: ステータス文字列(`"200 OK"`) - 第2引数: ヘッダーのリスト(タプルのリスト) ヘッダーを辞書ではなくタプルのリストにしているのは、同じ名前のヘッダーが複数回出現しうるためです。 **戻り値はイテラブルである。** アプリケーションは、レスポンスボディを**バイト列のイテラブル**として返します。 最もシンプルなのはバイト列を要素とするリストです。 ```python return [b"Hello, World!"] ``` リストではなくジェネレータを返すこともできます。 これにより、大きなレスポンスボディをメモリに一度に載せることなく、少しずつ生成してクライアントに送ることが可能になります。 ```python def application(environ, start_response): start_response("200 OK", [("Content-Type", "text/plain")]) yield b"Hello, " yield b"World!" ``` WSGI サーバの側から見ると、処理の流れは次のようになります。 ```{mermaid} sequenceDiagram participant S as WSGIサーバ participant A as アプリケーション S->>S: リクエストパース → environ 組み立て S->>A: application(environ, start_response) A->>S: start_response("200 OK", headers) 呼び出し A->>S: [b"Hello, World!"] を return S->>S: ステータス/ヘッダーを送信 S->>S: イテラブルを走査してボディを送信 ``` ```python # 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) ``` {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で書いた自作サーバの処理フローと見比べてみてください。 ソケットの操作(`accept`、`recv`、`sendall`)はサーバ側に閉じ込められ、アプリケーション側は `environ` と `start_response` という抽象的なインタフェースだけを通じてサーバとやりとりしています。 ソケットの存在すら知らないのです。 --- 本項では、WSGI の全体像を把握しました。 サーバとアプリケーションの間に明確な境界線を引き、その接点を「callable に `environ` と `start_response` を渡し、イテラブルを受け取る」というシンプルな約束事で定義する——この約束事が、Python の Web エコシステム全体を支える基盤です。 次項では、`environ` 辞書の中身を詳しく見ていきます。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバで `parse_request_line()` や `parse_headers()` を使って苦労して取り出していた情報が、`environ` の中にどのような形で格納されているかを確認しましょう。 (environ を読む)= ## environ を読む 前項で、WSGI アプリケーションが `environ` と `start_response` の2つの引数を受け取る callable であることを確認しました。 本項では、第1引数の `environ` 辞書の中身を詳しく見ていきます。 `environ` は、サーバがリクエストをパースして組み立てた情報の集合体です。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバでは、`parse_request_line()`、`parse_headers()`、`parse_full_path()` と複数の関数を使ってバイト列から情報を取り出していました。 WSGI では、その作業をサーバが済ませたうえで、すべてをひとつの辞書に詰めてアプリケーションに渡してくれます。 ### リクエスト情報の入り口 `environ` の中身を実際に見てみるのが最も早い理解の方法です。 ```{mermaid} flowchart LR R["HTTPリクエスト
生バイト列"] E["environ 辞書"] R --> E E --> M["REQUEST_METHOD
= 'GET'"] E --> P["PATH_INFO
= '/hello'"] E --> Q["QUERY_STRING
= 'name=Taro'"] E --> I["wsgi.input
(ボディストリーム)"] E --> H["HTTP_HOST
= '127.0.0.1:8000'"] ``` 次の WSGI アプリケーションは、`environ` のキーと値をすべて表示します。 ```python # 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` モジュールを使えば、このアプリケーションをすぐに動かせます。 ```python # 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='' ...> wsgi.input: <_io.BufferedReader ...> wsgi.multiprocess: False wsgi.multithread: True wsgi.run_once: False wsgi.url_scheme: 'http' ``` CGI に由来するキー(大文字)と WSGI 独自のキー(`wsgi.` プレフィックス)が混在していることがわかります。 これらのキーを、重要なものから順に見ていきましょう。 ```{note} 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 | マルチプロセス環境かどうか | ``` ### REQUEST_METHOD ```python method = environ["REQUEST_METHOD"] # "GET", "POST" など ``` HTTP リクエストのメソッドが文字列として格納されています。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバでは `parse_request_line()` でリクエストラインを空白で分割して最初の要素を取り出していましたが、WSGI ではサーバがその作業を済ませて `REQUEST_METHOD` に入れてくれています。 Django のビュー関数で `request.method` と書いたとき、Django は内部でこの `environ["REQUEST_METHOD"]` の値を返しています。 FastAPI の `@app.get()` デコレータも、内部ではこの値を見てメソッドのマッチングを行っています。 ### PATH_INFO ```python path = environ.get("PATH_INFO", "/") # "/hello", "/users/42" など ``` リクエストされたパスが格納されています。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で `parse_full_path()` を使ってクエリ文字列を切り離す処理を書きましたが、WSGI ではパスとクエリ文字列が最初から別のキーに分かれています。 `PATH_INFO` にはパス部分だけが入り、クエリ文字列は `QUERY_STRING` に入ります。 WSGI にはもうひとつ `SCRIPT_NAME` というキーがあり、アプリケーションが URL のルート(`/`)ではなくサブパス(たとえば `/myapp`)にマウントされている場合に、そのプレフィックスが入ります。 通常は空文字列です。クライアントがリクエストした完全なパスは `SCRIPT_NAME + PATH_INFO` になります。 ```python # アプリケーションが /myapp にマウントされている場合 # リクエスト: GET /myapp/users/42 environ["SCRIPT_NAME"] # "/myapp" environ["PATH_INFO"] # "/users/42" ``` この分離は、ひとつのサーバ上で複数の WSGI アプリケーションを異なるパスで同時に運用するときに意味を持ちます。 各アプリケーションは自分の `PATH_INFO` だけを見ればよく、`SCRIPT_NAME` のプレフィックスを意識する必要がありません。 ### QUERY_STRING ```python query_string = environ.get("QUERY_STRING", "") # "name=Taro&page=2" ``` URL の `?` 以降のクエリ文字列がそのまま格納されています。 「そのまま」というのは、パースされていない生の文字列だということです。`name=Taro&page=2` を辞書に変換する作業は、アプリケーション側(またはフレームワーク)の仕事です。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)では `urllib.parse.parse_qs()` を使ってこの変換を行いました。 ```python from urllib.parse import parse_qs query_params = parse_qs(environ.get("QUERY_STRING", "")) # {"name": ["Taro"], "page": ["2"]} ``` ```{note} WSGI の `environ` がクエリ文字列をパースせずに生の文字列のまま渡しているのは、意図的な設計です。 WSGI はサーバとアプリケーションの間の**最小限の**インタフェースを目指しており、クエリ文字列の解釈方法はアプリケーション側に委ねています。 Django が `request.GET` として `QueryDict` オブジェクトを提供し、FastAPI が関数の引数として型変換まで行ってくれるのは、フレームワークがこの生の文字列を加工した結果です。 ``` ### wsgi.input ```python body_stream = environ["wsgi.input"] ``` リクエストボディを読み取るためのファイルライクオブジェクトです。 `environ` の中で唯一、文字列ではなくオブジェクトが入っているキーのひとつです。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)では、`Content-Length` を見てボディのバイト数を判断し、`recv()` を繰り返し呼んでボディを受信するコードを書きました。 WSGI では、サーバがボディの受信を管理し、アプリケーションにはファイルライクオブジェクトとして提供します。 アプリケーションは `.read()` メソッドでボディを読み取ります。 ```python content_length = int(environ.get("CONTENT_LENGTH", "0") or "0") body = environ["wsgi.input"].read(content_length) ``` ```{warning} `CONTENT_LENGTH` が空文字列の場合があるため、`or "0"` でフォールバックしている点に注意してください。 WSGI の仕様では、`CONTENT_LENGTH` は存在しないか空文字列の場合があり、その場合はボディがないことを意味します。 また、`wsgi.input` からボディを読み取るときに `content_length` を指定しないと、ストリームの終端まで読もうとします。 持続的接続の場合、ストリームの終端が来るのはコネクションが閉じられるときであり、サーバがいつ閉じるかに依存します。 `Content-Length` で読み取るバイト数を明示するのが安全な方法です。 ``` {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で `Content-Length` に基づくボディの正確な受信がいかに重要かを体験しました。 WSGI では、ソケットからの受信はサーバが担当しますが、`wsgi.input` からの読み取りバイト数の管理はアプリケーション側の責任です。 フレームワークがこの管理を代行してくれるので、通常は意識する必要がありませんが、仕組みとして知っておくことはトラブルシューティングの場面で役立ちます。 ### SERVER_NAME, SERVER_PORT ```python 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` ではありません。 ``` ### 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" ``` {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバでは `parse_headers()` でヘッダーを辞書に変換し、キーを小文字に統一していました。 WSGI では逆に大文字に統一し、`HTTP_` プレフィックスを付けます。この変換規則は CGI から受け継いだものです。 ```{caution} 2つのヘッダーだけはこの規則の例外です。 | ヘッダー | environ のキー | 備考 | |----------|---------------|------| | `Content-Type` | `CONTENT_TYPE` | `HTTP_` プレフィックスなし | | `Content-Length` | `CONTENT_LENGTH` | `HTTP_` プレフィックスなし | これも CGI の仕様に由来する歴史的な慣例です。 `Content-Type` と `Content-Length` はリクエストの処理に不可欠なヘッダーであるため、CGI の時代から特別扱いされていました。 ``` ```python # ヘッダーの取得例 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"]` のような自然な形式でヘッダーにアクセスできるのは、フレームワークが `environ` の `HTTP_` プレフィックス付きのキーを逆変換しているからです。 この変換の存在を知っていると、「`environ` の中では `HTTP_HOST` だが、Django の `request.META` でも `HTTP_HOST` で、`request.headers` では `Host` になる」という対応関係が理解できます。 --- 本項では、`environ` 辞書に含まれる主要なキーを確認しました。 - `REQUEST_METHOD` にメソッド - `PATH_INFO` にパス - `QUERY_STRING` にクエリ文字列 - `wsgi.input` にボディのストリーム - `HTTP_*` にクライアントのヘッダー {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバで苦労して取り出していた情報が、WSGI ではすべて構造化された形で手に入ります。 次項では、`environ` と対になるもうひとつの重要な要素——`start_response` とイテラブルによるレスポンスの返却方法を詳しく見ていきます。 (start_response の役割)= ## start_response の役割 前項で、`environ` 辞書を通じてリクエスト情報を受け取る方法を確認しました。 本項では、レスポンスの前半部分——ステータスコードとヘッダー——をサーバに伝えるための仕組みである `start_response` を掘り下げます。 WSGI のレスポンスは、2つの経路に分かれて返されます。 ステータスとヘッダーは `start_response` を通じて、ボディはアプリケーション callable の戻り値として。 この分離には設計上の意図があり、それを理解することが WSGI の仕組みを正しく把握する鍵になります。 ### ステータス `start_response` の第1引数は、ステータスコードと理由フレーズを連結した文字列です。 ```python start_response("200 OK", [("Content-Type", "text/plain")]) ``` `"200 OK"` という文字列は、{numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で自作サーバの `build_status_line()` が組み立てていたステータスラインの一部に対応しています。 自作サーバでは `f"HTTP/1.1 {status_code} {reason}\r\n"` のように HTTP バージョンや改行を含む完全なステータスラインを作っていましたが、WSGI ではステータスコードと理由フレーズだけを渡します。 HTTP バージョンの付加や改行の処理はサーバの仕事であり、アプリケーションは関与しません。 ステータス文字列の形式は「3桁の数字、半角スペース、理由フレーズ」です。 ```python # 正しい形式 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"` のように標準的な理由フレーズを返します。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)では `REASON_PHRASES` 辞書を作ってステータスコードと理由フレーズの対応を管理していました。 フレームワークはこの対応を内部に持っており、`HttpResponse(status=404)` のように数値でステータスコードを指定するだけで、適切な理由フレーズを自動的に付けてくれます。 ### ヘッダー `start_response` の第2引数は、レスポンスヘッダーのリストです。各要素は `(名前, 値)` のタプルです。 ```python headers = [ ("Content-Type", "text/html; charset=utf-8"), ("Content-Length", "42"), ("X-Custom-Header", "some-value"), ] start_response("200 OK", headers) ``` 辞書ではなくタプルのリストを使う理由は、HTTP では同じ名前のヘッダーが複数回出現することがあり、辞書ではそれを表現できないためです。 ```python 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 ヘッダーの仕様に基づく制約です。 ```{caution} WSGI の仕様では、アプリケーションが返すヘッダーにいくつかの制約があります。 アプリケーションは `hop-by-hop` ヘッダー(`Transfer-Encoding`、`Connection` など)を返してはなりません。 これらはサーバが管理するヘッダーであり、アプリケーションが設定するとサーバの動作と矛盾する可能性があるためです。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバでは `Connection: close` を自分で設定していましたが、WSGI の世界では接続の管理はサーバの責任です。 ``` `Content-Length` については少し微妙な立場にあります。 アプリケーションが `Content-Length` を設定することは許されていますし、設定すべきとされています。 しかし、サーバがイテラブルの内容からボディの長さを計算して自動的に付加する場合もあります。 動作はサーバの実装に依存しますが、アプリケーション側で正確な `Content-Length` を設定しておくのが最も確実です。 ### ボディ本体との分離 ここで立ち止まって、「なぜステータスとヘッダーを戻り値ではなく `start_response` というコールバックで返す設計にしたのか」を考えてみましょう。 ```{mermaid} sequenceDiagram participant S as WSGIサーバ participant A as アプリケーション S->>A: application(environ, start_response) A->>S: start_response("200 OK", headers) Note over S: ステータス/ヘッダーを記録 A-->>S: yield b"chunk 1" S-->>S: クライアントに送信 A-->>S: yield b"chunk 2" S-->>S: クライアントに送信 ``` 次のような設計のほうが、一見シンプルに思えるかもしれません。 ```python # こういう設計ではダメだったのか? def application(environ): return ("200 OK", [("Content-Type", "text/plain")], [b"Hello"]) ``` ステータス、ヘッダー、ボディをタプルにまとめて返す。 この設計でも動くことは動きます。 しかし、WSGI が `start_response` をコールバックにした理由は、**レスポンスボディの生成が始まる前にステータスとヘッダーを確定させる**という HTTP の構造を反映するためです。 ```{important} HTTP レスポンスは、ステータスライン → ヘッダー → 空行 → ボディ、という順序で送信されます。 ボディの最初の1バイトを送る前に、ステータスとヘッダーがすべて確定している必要があります。 `start_response` をコールバックにすることで、アプリケーションは「まずステータスとヘッダーを通知し、その後にボディを生成する」という HTTP の順序を自然に表現できます。 ``` この設計が真価を発揮するのは、レスポンスボディをストリーミングで返す場合です。 ```python 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` があります。 ```python 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."] ``` ```{note} 通常、`start_response` は1回のリクエスト処理で1回だけ呼ばれます。 しかし、`exc_info` を渡す場合に限り、2回目の呼び出しが許されます。 これは、処理の途中でエラーが発生し、すでに `start_response` を呼んでいた場合に、ステータスをエラー用に差し替えるための仕組みです。 ただし、ボディの送信がすでに始まっていた場合(最初の `yield` がサーバに渡された後)は、ステータスの差し替えはできず、例外が再送出されます。 HTTP の性質上、ステータスラインはボディより先に送信されているため、一度送ったステータスは取り消せないからです。 ``` この仕組みは実務で直接使うことはほとんどありません。 フレームワークがエラーハンドリングを代行してくれるからです。 しかし、WSGI の設計が HTTP の構造——ステータスが先、ボディが後、一度送った先頭部分は取り消せない——を忠実に反映していることがわかります。 --- 本項では、`start_response` の役割を3つの観点から確認しました。 1. ステータス文字列によるステータスコードの通知 2. タプルのリストによるヘッダーの設定 3. ボディとの分離がもたらすストリーミング対応とエラーハンドリングの柔軟性 `environ` がリクエストの入口であるのに対し、`start_response` はレスポンスの出口の前半です。 次項では、出口の後半——イテラブルとしてのレスポンスボディ——を見ていきます。 アプリケーションの戻り値がなぜリストやジェネレータでなければならないのか、その設計の意図を理解しましょう。 (iterable としてのレスポンス)= ## iterable としてのレスポンス 前項で、`start_response` がステータスコードとヘッダーをサーバに伝える役割を担っていることを確認しました。 レスポンスの残りの部分——ボディ——は、アプリケーション callable の**戻り値**として返されます。 WSGI の仕様では、この戻り値は**バイト列のイテラブル**でなければならないと定められています。 単なるバイト列(`b"Hello"`)ではなく、イテラブル(`[b"Hello"]`)です。 この設計には明確な理由があります。 本項では、イテラブルとしてのレスポンスを、リストとジェネレータという2つの形式で実装しながら、その意図を理解していきます。 ### list を返す 最もシンプルで一般的なのは、バイト列を要素とするリストを返す方法です。 ```python def application(environ, start_response): start_response("200 OK", [("Content-Type", "text/plain")]) return [b"Hello, World!"] ``` リストの各要素はバイト列(`bytes` 型)でなければなりません。 文字列(`str` 型)を返すとエラーになります。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で繰り返し確認したとおり、HTTP レスポンスのボディはバイト列としてネットワーク上を流れるものであり、文字列はバイト列ではありません。 リストに複数の要素を入れることもできます。 ```python def application(environ, start_response): start_response("200 OK", [("Content-Type", "text/html; charset=utf-8")]) return [ b"", b"

Hello

", b"

World

", b"", ] ``` サーバはリストの各要素を順番に取り出し、HTTP レスポンスのボディとしてクライアントに送信します。 クライアントから見れば、これらの要素が連結されたひとつのボディとして見えます。 ```{warning} 「なぜ単にバイト列を返すのではなく、バイト列のリストを返すのか」という疑問が浮かぶかもしれません。 `return b"Hello, World!"` ではダメなのでしょうか。 技術的には、バイト列もイテラブルです。 Python では `bytes` 型を `for` ループで走査すると、**1バイトずつ整数として取り出されます**。 `for chunk in b"Hello": print(chunk)` は `72`、`101`、`108`、`108`、`111` と出力します。 つまり、サーバがバイト列をイテレートすると、1バイトずつ送信するという恐ろしく非効率な動作になってしまいます。 バイト列をリストに入れて `[b"Hello, World!"]` とすることで、リストの走査で得られるのはバイト列そのもの(チャンク)になり、まとまった単位で送信できます。 ``` `Content-Length` の計算は、リストの全要素の合計バイト数で行います。 ```python def application(environ, start_response): body_parts = [b"", b"

Hello

", b""] 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 のファイルを返す場合はどうでしょうか。 ファイル全体をメモリに読み込んでリストに入れるのは現実的ではありません。 ここで、ジェネレータの出番です。 ### generator を返す Python のジェネレータは、値を一度にすべて生成するのではなく、必要に応じてひとつずつ生成するイテラブルです。 WSGI のレスポンスとしてジェネレータを返すと、サーバは `yield` されたチャンクを受け取るたびにクライアントに送信できます。 ```python 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:` のようにジェネレータを走査し、各チャンクをクライアントに送信します。 ジェネレータの真価は、大きなデータを扱うときに発揮されます。 たとえば、大きなファイルを返す場合を考えてみましょう。 ```python 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` を待ちます。 リストで同じことをしようとすると、こうなります。 ```python # メモリに全体を載せてしまう — 大きなファイルでは危険 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] ``` 動作としては正しいですが、ファイルサイズが大きくなるとメモリを圧迫します。 複数のリクエストが同時に大きなファイルをダウンロードしている状況では、サーバのメモリが枯渇する危険があります。 ```{note} WSGI の仕様では、イテラブルが `close()` メソッドを持っている場合、サーバはイテレーションの完了後に `close()` を呼ばなければならないと定められています。 ジェネレータは `close()` メソッドを持っているので、サーバはジェネレータの走査が終わった後に自動的にクリーンアップを行います。 ファイルを開いたジェネレータの場合、`with` 文の中で `yield` しているため、ジェネレータの `close()` 呼び出しによって `with` ブロックから抜け、ファイルが確実に閉じられます。 ``` ### ストリーミングの考え方 ジェネレータを使ったレスポンスは、**ストリーミング**という概念につながります。 ストリーミングとは、レスポンス全体の生成を待たずに、準備できた部分から順次クライアントに送り出す手法です。 ```{mermaid} sequenceDiagram participant C as クライアント participant S as WSGIサーバ participant A as アプリ(ジェネレータ) S->>A: イテレーション開始 A-->>S: yield b"id,name
" S-->>C: チャンク送信 A-->>S: yield b"1,Taro
" S-->>C: チャンク送信 A-->>S: yield b"2,Hanako
" S-->>C: チャンク送信 A-->>S: StopIteration S->>A: close() ``` 通常のリスト返却では、アプリケーションがリスト全体を生成し終わるまで、サーバはクライアントへの送信を始められません。 一方、ジェネレータを使えば、最初の `yield` が実行された時点で、サーバはそのチャンクをクライアントに送信できます。 この違いが意味を持つ典型的なシナリオがあります。 データベースから大量のレコードを取得して CSV として返す API を想像してください。 ```python 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` は、まさにこの仕組みを利用しています。 ``` ```python # 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 アプリケーションを自分の手で書いてみましょう。 (最小の WSGI アプリを書く)= ## 最小の WSGI アプリを書く ここまでの3つの項で、WSGI の構成要素——`environ`、`start_response`、イテラブルレスポンス——をひとつずつ確認してきました。 本項では、これらを組み合わせて実際に動く WSGI アプリケーションを書きます。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で自作した HTTP サーバと対比しながら進めます。 あのとき苦労して書いたソケット操作やバイト列のパースが、WSGI によってどれだけ消えるのかを体感してください。 ### Hello World まずは最小限の WSGI アプリケーションです。 ```python # 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` で動かします。 ```python # 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() ``` ```bash python run_hello.py ``` 別のターミナルから `curl` で確認します。 ```bash 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! ``` ```{important} {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 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 で動かすこともできます。 ```bash pip install gunicorn gunicorn hello_wsgi:application ``` アプリケーションのコードは1文字も変えていません。 サーバだけが `wsgiref` から Gunicorn に入れ替わりました。 「共通インタフェースによる疎結合」が、ここで実現しています。 ### ルーティング付き最小実装 Hello World だけでは物足りないので、パスとメソッドに基づくルーティングを追加しましょう。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で `if ... elif ... else` の連鎖で書いたルーティングを、WSGI アプリケーションとして書き直します。 ```python # 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] ``` ```bash 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 ``` {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバのルーティングと見比べてみてください。 ロジックの構造はほぼ同じです。`method` と `path` の取得方法が変わっただけです。 自作サーバでは `parse_request_line(header_part)` を呼んでバイト列からメソッドとパスを取り出していましたが、WSGI では `environ["REQUEST_METHOD"]` と `environ["PATH_INFO"]` から直接取り出せます。 ただし、このコードには明らかな冗長さがあります。 すべてのルートで `start_response` の呼び出しと `Content-Length` の計算が繰り返されています。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)で作った `make_response()` のようなヘルパーが欲しくなります。 ```python # 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}") ``` ```{admonition} コラム: フレームワークへの自然な進化 :class: tip `make_text_response()` はエンコーディングと `Content-Length` の計算を隠蔽し、呼び出し側をすっきりさせています。 この小さなヘルパーは、フレームワークが提供する `HttpResponse` や `Response` の最も原始的な祖先と言えるかもしれません。 コードの冗長さを解消しようとすると、自然とフレームワークの方向に向かっていくのです。 ``` ### JSON を返す例 REST API では JSON レスポンスが標準的です。 WSGI アプリケーションで JSON を返す例を書いてみましょう。 ```python # json_wsgi.py import json from urllib.parse import parse_qs USERS = { 1: {"id": 1, "name": "Taro Yamada", "email": "taro@example.com"}, 2: {"id": 2, "name": "Hanako Sato", "email": "hanako@example.com"}, 3: {"id": 3, "name": "Jiro Suzuki", "email": "jiro@example.com"}, } 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` で動作を確認します。 ```bash 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": "hanako@example.com"} 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": "yuki@example.com"}' # → {"id": 4, "name": "Yuki Tanaka", "email": "yuki@example.com"} ``` {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバで書いた JSON API と比較してみてください。 ルーティングやボディのパースといったアプリケーションロジックはほぼ同じですが、ソケット操作が完全に消えています。 - `recv()` の代わりに `environ["wsgi.input"].read()` がある - `sendall()` の代わりにイテラブルの `return` がある POST のボディ読み取りでは、`CONTENT_LENGTH` と `wsgi.input` を使っています。 `environ.get("CONTENT_LENGTH", "0") or "0"` という書き方は少し冗長ですが、`CONTENT_LENGTH` が空文字列の場合に対応するためのものです。 こうした細かな防御的コードが、フレームワークを使えば `request.json()` の一言で済むことを思うと、フレームワークのありがたみを改めて感じます。 ```{note} このコードのルーティング部分を見ると、`path.startswith("/users/")` でパスの先頭一致を判定し、`path.split("/")[2]` でパスパラメータを取り出すという素朴な実装になっています。 - `/users/42/profile` のようなネストしたパスには対応できない - パスパラメータの型変換も自分で書く必要がある Django の `` や FastAPI の `{user_id}: int` が、こうした面倒をどれほど引き受けてくれているかが実感できるのではないでしょうか。 ``` --- 本項では、WSGI アプリケーションを Hello World から始め、ルーティング、クエリパラメータ、JSON レスポンス、POST ボディの受信まで実装しました。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)の自作サーバとの対比で、WSGI がサーバの責務(ソケット操作、HTTP パース)をアプリケーションから切り離してくれていることを確認し、同時にルーティングの冗長さやヘルパー関数への欲求を通じて、WSGI の上にフレームワークが欲しくなる感覚も体験しました。 次項では、WSGI のもうひとつの重要な概念——ミドルウェアについて見ていきます。 サーバとアプリケーションの間に「もう一枚の層」を挟むことで、横断的な関心事をどう扱うかを学びます。 (WSGI の良さと限界)= ## WSGI の良さと限界 {numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)を通じて、WSGI の背景、仕様、そして実装を見てきました。 `environ` と `start_response` というシンプルなインタフェースが、Python の Web エコシステムを支えてきたことを実感できたのではないでしょうか。 本項では、WSGI を一歩引いた視点から評価します。 WSGI の何が優れていて、どこに限界があるのか。 この整理は、Vol.2「なぜ ASGI が必要になったのか」で出会う ASGI がなぜ必要とされたのかを理解するための布石になります。 ### シンプルさ WSGI の最大の美点は、仕様の徹底的なシンプルさです。 アプリケーション側の要件を思い出してください。 「2つの引数を受け取る callable であり、`start_response` を呼んでからバイト列のイテラブルを返す」。 これがすべてです。 特別な基底クラスを継承する必要もなければ、特定のデコレータを適用する必要もありません。 Python の関数をひとつ書くだけで、WSGI アプリケーションが完成します。 ```python def application(environ, start_response): start_response("200 OK", [("Content-Type", "text/plain")]) return [b"Hello"] ``` この3行に、WSGI の仕様が要求するすべてが含まれています。 PEP 3333 の仕様書自体も、他のプロトコル仕様と比べると驚くほど短い文書です。 シンプルさは次のような恩恵をもたらします。 - **学習の容易さ**: `environ`、`start_response`、イテラブルという3つの概念を学ぶだけで全体像を把握できる - **実装の容易さ**: WSGI サーバ、アプリケーション、ミドルウェアのどれを書くにも、特別に複雑なコードは不要 ```{note} このシンプルさは偶然の産物ではなく、意図的な設計判断です。 PEP 333 の著者 Phillip Eby は、WSGI を「フレームワークの著者が使うための低レベルのインタフェース」と位置づけ、あえて高レベルの機能(セッション管理やテンプレートエンジンなど)を仕様に含めませんでした。 最小限のインタフェースだけを定義し、それ以上のことはフレームワークに委ねる。 この割り切りが、WSGI の20年以上にわたる長寿命につながっています。 ``` ### 相互運用性 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 のおかげです。 アプリケーションのコードは一切変更する必要がありません。 ``` ### 同期前提 WSGI の限界は、その設計の前提にあります。 WSGI は**同期処理を前提**として設計されています。 ```{mermaid} flowchart TD A[リクエスト受信] --> B[DB問い合わせ] B --> C["I/O 待ち 100ms
⚠ ワーカー停止中"] C --> D[外部 API 呼び出し] D --> E["I/O 待ち 200ms
⚠ ワーカー停止中"] E --> F[レスポンス返却] ``` 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 を想定していません。 ```{danger} 次のコードは WSGI として動作しません。 ```python # これは 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")] ``` `environ` と `start_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 の精神——サーバとアプリケーションの間の共通インタフェース——を受け継ぎつつ、非同期処理と双方向通信を仕様の中に取り込んでいます。 その前に、次の{numref}`WSGI の上に何が必要になるのか`({ref}`WSGI の上に何が必要になるのか`)では WSGI の上に構築されたフレームワークの内側を見ていきます。 Werkzeug、Bottle、そして Flask が、WSGI の `environ` と `start_response` をどのように包み込んで開発者に優しいインタフェースを提供しているのかを確認しましょう。 (ch04-トラブルシューティングの観点)= ## トラブルシューティングの観点 {numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)の締めくくりとして、WSGI の知識が実際のトラブルシューティングでどう役立つかを具体的に見ていきます。 フレームワークを使った日常の開発では、`environ` や `start_response` を直接触ることはほとんどありません。 しかし、フレームワークの動作が期待どおりにならないとき、一段下の WSGI レイヤーに降りて確認することで、問題の原因が見えることがあります。 {numref}`まずは 1 リクエストだけ処理するサーバを作る`({ref}`まずは 1 リクエストだけ処理するサーバを作る`)のトラブルシューティングの項で「生の HTTP を見る」ことの価値を議論しましたが、WSGI はその HTTP とフレームワークの間にある層です。 HTTP の生のバイト列よりは扱いやすく、フレームワークの抽象よりは生々しい——ちょうど良い観察ポイントなのです。 ### environ をダンプする 「リクエストの情報がフレームワークに正しく届いているかわからない」——こうした状況で最も直接的な手段は、`environ` の中身をそのまま出力することです。 `environ` のダンプアプリケーションを書きましたが、フレームワークのビュー関数の中からでも同じことができます。 Django であれば `request.META` が `environ` をほぼそのまま保持しています。 ```python # 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` を覗きます。 ```python # 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` が標準エラー出力にダンプされます。 問題の調査が終わったら外す、という使い捨ての道具です。 ```python # Gunicorn で使う場合 from myapp.wsgi import application from environ_debug_middleware import EnvironDebugMiddleware application = EnvironDebugMiddleware(application) ``` ```{tip} `environ` をダンプすると、次のような問題をたちどころに発見できます。 - リバースプロキシの設定ミスで `HTTP_HOST` の値がおかしくなっている - `SCRIPT_NAME` が意図しない値になっている - HTTPS 環境なのに `wsgi.url_scheme` が `http` のままになっている ``` ### ヘッダーがどこに入るか 「クライアントが送ったはずのカスタムヘッダーが、フレームワーク側で取得できない」という問題は、WSGI のヘッダー変換規則を知らないと混乱しがちです。 HTTP ヘッダーは `environ` に格納される際に変換を受けます。 大文字に変換し、ハイフンをアンダースコアに置き換え、`HTTP_` プレフィックスを付ける。 たとえば、クライアントが `X-Request-Id: abc123` というカスタムヘッダーを送った場合、`environ` では `HTTP_X_REQUEST_ID` というキーに格納されます。 ```bash curl -H "X-Request-Id: abc123" http://127.0.0.1:8000/ ``` ```python # 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` のキーを変換しているだけです。 ```{caution} この変換規則を知っていると、次のような問題を瞬時に診断できます。 **よくある間違い**: 「`Authorization` ヘッダーを送っているのに、Django の `request.META` で取得できない」 → `request.META["Authorization"]` で探していませんか。正しくは `request.META["HTTP_AUTHORIZATION"]` です。 **もうひとつの落とし穴**: `Content-Type` と `Content-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`)ため、リバースプロキシ経由の環境ではそもそもアンダースコア入りのヘッダーが届かないこともあります。 ### body 読み取りミス POST リクエストのボディを扱うとき、`wsgi.input` からの読み取りに関連する問題がいくつかあります。 ```{warning} 最もよくある問題は、**ボディを2回読もうとする**ことです。 `wsgi.input` はファイルライクオブジェクトであり、`.read()` を呼ぶとストリームの現在位置が進みます。 一度読んだ後にもう一度 `.read()` を呼んでも、空のバイト列が返ります。 ```python 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_LENGTH` や `wsgi.input` のレイヤーまで降りて原因を追えるのです。 ### iterable を返す意味の誤解 WSGI アプリケーションの戻り値に関して、いくつかの典型的な間違いがあります。 ```{caution} **間違い1: バイト列をリストに入れずに直接返す** ```python # 間違い — バイト列を直接返している def application(environ, start_response): start_response("200 OK", [("Content-Type", "text/plain")]) return b"Hello, World!" ``` `bytes` 型はイテラブルですが、走査すると1バイトずつ整数が返されます。 サーバが `for chunk in result:` で走査すると、各チャンクが整数(`72`、`101`、`108`...)になり、バイト列として送信できずにエラーになります。 正しくは `return [b"Hello, World!"]` です。 **間違い2: 文字列のリストを返す** ```python # 間違い — 文字列を返している 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() # これがないとファイルが閉じられない ``` ```{danger} `close()` を実装し忘れると、ファイルディスクリプタがリークします。 リクエストのたびにファイルが開かれ、閉じられないまま蓄積し、最終的に OS の「開けるファイル数の上限」に達してサーバが新しいリクエストを処理できなくなります。 本番環境で実際に起こりうる問題ですので、カスタムイテラブルを作る際は必ず `close()` を実装してください。 ``` --- 本項では、WSGI のレイヤーで発生しやすいトラブルとその診断方法を確認しました。 - `environ` のダンプによるリクエスト情報の確認 - ヘッダーの変換規則(`HTTP_` プレフィックスと例外) - `wsgi.input` のストリーム特性(1回しか読めない) - イテラブルの正しい返し方(バイト列のリスト) いずれも、WSGI の仕様を理解していれば自然と避けられる問題であり、理解していなければ原因の特定に苦労する問題です。 {numref}`WSGI が生まれた背景`({ref}`WSGI が生まれた背景`)はここで終わりです。 WSGI が生まれた背景から仕様の詳細、最小実装、そしてトラブルシューティングまでを一気に駆け抜けました。 次の{numref}`WSGI の上に何が必要になるのか`({ref}`WSGI の上に何が必要になるのか`)では、WSGI の上に構築されたフレームワーク——Werkzeug、Bottle、Flask——の内部を覗きます。 本章で学んだ `environ` と `start_response` が、フレームワークの中でどのように包み込まれ、開発者にとって使いやすいインタフェースに変換されているのかを見ていきましょう。