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'

requestg などのコンテキストオブジェクトは、必ずリクエストハンドラ内でのみ使用し、スレッドやタスクをまたいで渡さないことが鉄則である。