WSGI アプリケーションを自作する
WSGI(Web Server Gateway Interface)の仕様を理解する最良の方法は、実際に動くアプリケーションを自分で書いてみることだ。フレームワークを使わず、素の Python だけで WSGI アプリケーションを構築する過程を通じて、Web アプリケーションの本質的な動作原理が見えてくる。
最小限の WSGI アプリケーション
WSGI アプリケーションの実体は、呼び出し可能なオブジェクト(callable)である。関数でもクラスでも、呼び出し可能であれば何でも構わない。
def application(environ, start_response):
status = '200 OK'
headers = [('Content-Type', 'text/plain; charset=utf-8')]
start_response(status, headers)
return [b'Hello, WSGI!']このたった 5 行のコードが、完全に動作する WSGI アプリケーションだ。environ はリクエスト情報を含む辞書、start_response はレスポンスヘッダーを設定するためのコールバック関数である。戻り値はレスポンスボディをバイト列のイテラブルとして返す。
標準ライブラリの wsgiref を使えば、このアプリケーションをすぐに動かせる。
from wsgiref.simple_server import make_server
def application(environ, start_response):
status = '200 OK'
headers = [('Content-Type', 'text/plain; charset=utf-8')]
start_response(status, headers)
return [b'Hello, WSGI!']
if __name__ == '__main__':
server = make_server('localhost', 8000, application)
print('Serving on http://localhost:8000')
server.serve_forever()python app.py を実行し、ブラウザで http://localhost:8000 にアクセスすれば「Hello, WSGI!」と表示される。
ルーティングの実装
実用的なアプリケーションには、URL に応じて異なる処理を行うルーティング機能が必要になる。environ['PATH_INFO'] からリクエストパスを取得し、条件分岐で処理を振り分ける。
def application(environ, start_response):
path = environ['PATH_INFO']
if path == '/':
status = '200 OK'
body = b'Welcome to the homepage!'
elif path == '/about':
status = '200 OK'
body = b'About this application'
elif path == '/users':
status = '200 OK'
body = b'User list'
else:
status = '404 Not Found'
body = b'Page not found'
headers = [('Content-Type', 'text/plain; charset=utf-8')]
start_response(status, headers)
return [body]この方法は単純だが、パスが増えると管理が困難になる。辞書を使ったルーティングテーブルを導入すれば、より整理された構造になる。
def home(environ):
return '200 OK', b'Welcome to the homepage!'
def about(environ):
return '200 OK', b'About this application'
def users(environ):
return '200 OK', b'User list'
routes = {
'/': home,
'/about': about,
'/users': users,
}
def application(environ, start_response):
path = environ['PATH_INFO']
handler = routes.get(path)
if handler:
status, body = handler(environ)
else:
status = '404 Not Found'
body = b'Page not found'
headers = [('Content-Type', 'text/plain; charset=utf-8')]
start_response(status, headers)
return [body]ハンドラー関数を分離したことで、各エンドポイントの処理を独立して記述できるようになった。
HTTP メソッドの判別
RESTful な API を構築するには、GET / POST / PUT / DELETE などの HTTP メソッドを判別する必要がある。environ['REQUEST_METHOD'] でメソッドを取得できる。
def users_handler(environ):
method = environ['REQUEST_METHOD']
if method == 'GET':
return '200 OK', b'{"users": ["alice", "bob"]}'
elif method == 'POST':
return '201 Created', b'{"message": "User created"}'
elif method == 'DELETE':
return '200 OK', b'{"message": "User deleted"}'
else:
return '405 Method Not Allowed', b'{"error": "Method not allowed"}'メソッドとパスの組み合わせでルーティングするには、タプルをキーとした辞書を使う方法がある。
routes = {
('GET', '/'): home_get,
('GET', '/users'): users_get,
('POST', '/users'): users_post,
('DELETE', '/users'): users_delete,
}
def application(environ, start_response):
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
key = (method, path)
handler = routes.get(key)
# 以下略リクエストボディの読み取り
POST リクエストなどで送信されたデータは、environ['wsgi.input'] から読み取る。このオブジェクトはファイルライクオブジェクトであり、read() メソッドでバイト列を取得できる。
def get_request_body(environ):
try:
content_length = int(environ.get('CONTENT_LENGTH', 0))
except ValueError:
content_length = 0
if content_length > 0:
return environ['wsgi.input'].read(content_length)
return b''CONTENT_LENGTH が設定されていない場合や不正な値の場合に備えて、エラーハンドリングを入れている。JSON データを受け取る場合は、さらにパースが必要になる。
import json
def users_post(environ):
body = get_request_body(environ)
try:
data = json.loads(body.decode('utf-8'))
name = data.get('name', 'Unknown')
return '201 Created', json.dumps({'created': name}).encode('utf-8')
except json.JSONDecodeError:
return '400 Bad Request', b'{"error": "Invalid JSON"}'クエリパラメータの解析
URL のクエリ文字列(?key=value&foo=bar の部分)は environ['QUERY_STRING'] に格納されている。標準ライブラリの urllib.parse を使って解析できる。
from urllib.parse import parse_qs
def search(environ):
query_string = environ.get('QUERY_STRING', '')
params = parse_qs(query_string)
# params は {'key': ['value1', 'value2'], ...} の形式
keyword = params.get('q', [''])[0]
page = params.get('page', ['1'])[0]
result = f'Search: {keyword}, Page: {page}'
return '200 OK', result.encode('utf-8')parse_qs は同じキーが複数回現れる可能性を考慮して、値をリストで返す。単一の値が欲しい場合は [0] でアクセスするか、parse_qs(query_string, keep_blank_values=True) のオプションを調整する。
クラスベースのアプリケーション
関数ベースのアプリケーションが大きくなってきたら、クラスにまとめると管理しやすくなる。__call__ メソッドを実装すれば、インスタンスを WSGI アプリケーションとして使える。
class Application:
def __init__(self):
self.routes = {}
def route(self, path, methods=None):
if methods is None:
methods = ['GET']
def decorator(func):
for method in methods:
self.routes[(method, path)] = func
return func
return decorator
def __call__(self, environ, start_response):
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
handler = self.routes.get((method, path))
if handler:
status, body = handler(environ)
else:
status = '404 Not Found'
body = b'Not Found'
headers = [('Content-Type', 'text/plain; charset=utf-8')]
start_response(status, headers)
return [body]
app = Application()
@app.route('/')
def home(environ):
return '200 OK', b'Home'
@app.route('/users', methods=['GET', 'POST'])
def users(environ):
if environ['REQUEST_METHOD'] == 'GET':
return '200 OK', b'User list'
else:
return '201 Created', b'User created'デコレータを使ったルーティング登録は、Flask などのフレームワークでおなじみのパターンだ。このように、フレームワークが提供する便利な機能も、根底では WSGI の仕組みの上に構築されている。
まとめ
WSGI アプリケーションは、environ と start_response を受け取り、レスポンスボディのイテラブルを返すという単純なインターフェースに基づいている。
関数一つで WSGI アプリケーションを作れる。start_response でステータスとヘッダーを設定し、バイト列のリストを返すだけ。
ルーティング、メソッド判別、リクエストボディ解析、クエリパラメータ処理を追加することで、実用的なアプリケーションに成長させられる。
フレームワークを使わずに WSGI アプリケーションを書く経験は、Flask や Django の内部で何が起きているかを理解する土台となる。












