WSGI ミドルウェアの仕組み
WSGI ミドルウェアは、WSGI アプリケーションとサーバーの間に挿入される中間層である。リクエストの前処理やレスポンスの後処理を、アプリケーション本体のコードを変更せずに追加できる仕組みだ。認証、ロギング、エラーハンドリング、セッション管理など、多くの横断的関心事(cross-cutting concerns)をミドルウェアとして実装できる。
ミドルウェアの基本構造
ミドルウェアは「WSGI アプリケーションをラップして、別の WSGI アプリケーションを返す」という構造を持つ。つまり、ミドルウェア自身も WSGI アプリケーションであり、内部に別のアプリケーションを保持している。
class SimpleMiddleware:
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
# リクエストの前処理をここに書く
print(f"Request: {environ['REQUEST_METHOD']} {environ['PATH_INFO']}")
# 元のアプリケーションを呼び出す
response = self.app(environ, start_response)
# レスポンスの後処理をここに書く
print("Response sent")
return responseこのミドルウェアを適用するには、元のアプリケーションをラップする。
def application(environ, start_response):
status = '200 OK'
headers = [('Content-Type', 'text/plain')]
start_response(status, headers)
return [b'Hello, World!']
# ミドルウェアを適用
wrapped_app = SimpleMiddleware(application)wrapped_app を WSGI サーバーに渡せば、すべてのリクエストとレスポンスがミドルウェアを通過する。
ミドルウェアのスタック
複数のミドルウェアを重ねて適用することで、機能を積み上げていける。これを「ミドルウェアスタック」と呼ぶ。
app = application
app = LoggingMiddleware(app)
app = AuthMiddleware(app)
app = ErrorHandlerMiddleware(app)リクエストは外側のミドルウェアから順に通過し、レスポンスは内側から外側へと戻っていく。
リクエスト受信
ErrorHandlerMiddleware → AuthMiddleware → LoggingMiddleware → application
レスポンス返却
この構造により、関心事を分離しながら柔軟に機能を組み合わせられる。
実践的なミドルウェア例
ロギングミドルウェア
リクエストの情報とレスポンス時間を記録するミドルウェアを作成する。
import time
class LoggingMiddleware:
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
start_time = time.time()
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
# レスポンスステータスを捕捉するためのラッパー
response_status = []
def custom_start_response(status, headers, exc_info=None):
response_status.append(status)
return start_response(status, headers, exc_info)
response = self.app(environ, custom_start_response)
elapsed = time.time() - start_time
status = response_status[0] if response_status else 'unknown'
print(f"{method} {path} - {status} - {elapsed:.3f}s")
return responsestart_response をラップすることで、アプリケーションが設定したステータスコードを取得している。これはミドルウェアでよく使われるテクニックだ。
認証ミドルウェア
特定のパスへのアクセスを制限する認証ミドルウェアの例を示す。
import base64
class BasicAuthMiddleware:
def __init__(self, app, username, password, protected_paths=None):
self.app = app
self.username = username
self.password = password
self.protected_paths = protected_paths or ['/admin', '/api']
def __call__(self, environ, start_response):
path = environ['PATH_INFO']
# 保護対象のパスかチェック
needs_auth = any(path.startswith(p) for p in self.protected_paths)
if needs_auth:
auth_header = environ.get('HTTP_AUTHORIZATION', '')
if not self._check_auth(auth_header):
status = '401 Unauthorized'
headers = [
('Content-Type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="Protected"')
]
start_response(status, headers)
return [b'Authentication required']
return self.app(environ, start_response)
def _check_auth(self, auth_header):
if not auth_header.startswith('Basic '):
return False
try:
encoded = auth_header[6:]
decoded = base64.b64decode(encoded).decode('utf-8')
user, pwd = decoded.split(':', 1)
return user == self.username and pwd == self.password
except Exception:
return False認証に失敗した場合は、元のアプリケーションを呼び出さずに 401 レスポンスを返す。このように、ミドルウェアはリクエストを途中で遮断することもできる。
CORS ミドルウェア
クロスオリジンリクエストを許可するための CORS ヘッダーを付与するミドルウェアだ。
class CORSMiddleware:
def __init__(self, app, allowed_origins=None):
self.app = app
self.allowed_origins = allowed_origins or ['*']
def __call__(self, environ, start_response):
origin = environ.get('HTTP_ORIGIN', '')
# プリフライトリクエストの処理
if environ['REQUEST_METHOD'] == 'OPTIONS':
status = '200 OK'
headers = self._get_cors_headers(origin)
headers.append(('Content-Length', '0'))
start_response(status, headers)
return [b'']
# CORS ヘッダーを追加するラッパー
def custom_start_response(status, headers, exc_info=None):
cors_headers = self._get_cors_headers(origin)
headers = list(headers) + cors_headers
return start_response(status, headers, exc_info)
return self.app(environ, custom_start_response)
def _get_cors_headers(self, origin):
if '*' in self.allowed_origins:
allow_origin = '*'
elif origin in self.allowed_origins:
allow_origin = origin
else:
allow_origin = ''
return [
('Access-Control-Allow-Origin', allow_origin),
('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'),
('Access-Control-Allow-Headers', 'Content-Type, Authorization'),
]プリフライトリクエスト(OPTIONS メソッド)を処理し、通常のリクエストには CORS ヘッダーを付与している。
レスポンスボディを加工するミドルウェア
レスポンスボディを加工するミドルウェアは、やや複雑になる。WSGI のレスポンスはイテラブルなので、それを消費して加工し、新たなイテラブルを返す必要がある。
class GzipMiddleware:
def __init__(self, app, min_length=500):
self.app = app
self.min_length = min_length
def __call__(self, environ, start_response):
# クライアントが gzip をサポートしているか確認
accept_encoding = environ.get('HTTP_ACCEPT_ENCODING', '')
if 'gzip' not in accept_encoding:
return self.app(environ, start_response)
response_started = []
captured_headers = []
def custom_start_response(status, headers, exc_info=None):
response_started.append(status)
captured_headers.extend(headers)
# 実際の start_response は後で呼ぶ
return lambda s: None # write() は使わない想定
# レスポンスボディを収集
response = self.app(environ, custom_start_response)
body = b''.join(response)
# 小さいレスポンスは圧縮しない
if len(body) < self.min_length:
start_response(response_started[0], captured_headers)
return [body]
# gzip 圧縮
import gzip
compressed = gzip.compress(body)
# ヘッダーを更新
new_headers = [
(k, v) for k, v in captured_headers
if k.lower() != 'content-length'
]
new_headers.append(('Content-Encoding', 'gzip'))
new_headers.append(('Content-Length', str(len(compressed))))
start_response(response_started[0], new_headers)
return [compressed]レスポンスボディを一度メモリに読み込む必要があるため、大きなファイルのストリーミングには適さない。用途に応じた設計が求められる。
関数スタイルのミドルウェア
クラスではなく関数でミドルウェアを書くこともできる。クロージャを活用したシンプルな書き方だ。
def timing_middleware(app):
def middleware(environ, start_response):
import time
start = time.time()
response = app(environ, start_response)
elapsed = time.time() - start
print(f"Request took {elapsed:.3f}s")
return response
return middleware
# 使用例
app = timing_middleware(application)デコレータとして使う場合も自然に書ける。
def with_timing(app):
def wrapper(environ, start_response):
import time
start = time.time()
result = app(environ, start_response)
print(f"Elapsed: {time.time() - start:.3f}s")
return result
return wrapper
@with_timing
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return [b'Hello']ミドルウェア設計のポイント
ミドルウェアを設計する際に意識すべき点をまとめる。
一つのミドルウェアには一つの責任だけを持たせる。ロギングと認証を同じミドルウェアに詰め込まない。
ミドルウェアの適用順序は動作に影響する。例えば、認証ミドルウェアはロギングミドルウェアの内側に置くことで、認証失敗もログに記録できる。
また、start_response の exc_info 引数を正しく扱うことも重要だ。例外発生時にヘッダーを再送信するケースに対応するためである。
ミドルウェアパターンを理解すれば、Flask の before_request や Django の MIDDLEWARE 設定が何をしているのか、より深く理解できるようになる。



