Flask で request オブジェクトをスレッド間で共有してはいけない
Flask の request オブジェクトはリクエストコンテキストに紐づいており、スレッド間で共有してはいけない。これを誤ると、あるユーザーのリクエスト情報が別のユーザーに漏洩する可能性がある。
request オブジェクトの特性
request は見た目上グローバルだが、実際は Werkzeug の LocalProxy により、スレッド/コルーチンごとに異なるオブジェクトを返す仕組みになっている。
from flask import request
@app.route('/process')
def process():
# この request は現在のリクエストコンテキストに紐づいている
user_input = request.form['data']
return f'Received: {user_input}'危険なパターン: バックグラウンドスレッドへの渡し
import threading
from flask import request
@app.route('/async')
def async_task():
# 危険: request をそのままスレッドに渡す
thread = threading.Thread(target=process_in_background, args=(request,))
thread.start()
return 'Task started'
def process_in_background(req):
# このスレッドではリクエストコンテキストが存在しない
# req へのアクセスは予期しない結果を招く
data = req.form['data'] # 動作しないか、別リクエストのデータになる可能性なぜ問題なのか
リクエストコンテキストはレスポンスを返した時点でポップされる。バックグラウンドスレッドがまだ実行中でも、コンテキストは破棄される。
| コンテキスト消失 | リクエスト終了後に request にアクセスするとエラー |
| データ漏洩 | 次のリクエストのデータが混入する可能性 |
| レースコンディション | 複数スレッドが同じコンテキストを参照 |
正しいアプローチ: 必要な値をコピーする
request そのものではなく、必要なデータを取り出してから渡す。
@app.route('/async')
def async_task():
# 必要な値をコピー
data = request.form['data']
user_id = request.headers.get('X-User-ID')
thread = threading.Thread(target=process_in_background, args=(data, user_id))
thread.start()
return 'Task started'
def process_in_background(data, user_id):
# プリミティブな値なので安全
print(f'Processing {data} for user {user_id}')Celery でのパターン
バックグラウンドタスクには必要な引数だけを渡す。
@celery.task
def send_email(to, subject, body):
# request には依存しない
mail.send(to=to, subject=subject, body=body)
@app.route('/send')
def send():
# 必要な値を抽出してタスクに渡す
send_email.delay(
to=request.form['email'],
subject=request.form['subject'],
body=request.form['body']
)
return 'Email queued'request や g などのコンテキストオブジェクトは、必ずリクエストハンドラ内でのみ使用し、スレッドやタスクをまたいで渡さないことが鉄則である。



