Niquestsの深刻なバグについての注意 (urllib3-future 2.15.902 で修正済み)

最近何度か紹介していた Niquests に致命的なバグがあり、問題の大きさの割にアナウンスが小さいので解説しておきます。

なお、このバグは urllib3-future 2.14.906 (2025-11-06) から発生し、2.15.902 (2026-02-03) で修正されました。 Niquests を使っている人は利用している urllib3-future のバージョンの確認を強く推奨します。

バグの内容

niquestsのSessionを使って同一ホストに対して並行にリクエストを送信したとき、その接続が HTTP/2 か HTTP/3 だと、レスポンスの取り違えが発生します。

再現コード

import niquests
import threading
import time
import sys
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("main")

session = niquests.Session(pool_maxsize=1, keepalive_delay=60)

stop = False


def get_worker(name):
    while not stop:
        response = session.get(f"https://httpbin.org/delay/1?name={name}")
        if response.status_code != 200:
            logger.error("bad status. code=%s, %s, body=%s", response.status_code, response.status, response.text)
        json = response.json()
        if json["args"]["name"] != name:
            logger.error(f"wrong name! got {json['args']['name']}, expected {name}")


def main():
    threads = []
    for i in range(4):
        t = threading.Thread(target=get_worker, args=(f"worker-{i}",))
        t.start()
        threads.append(t)

    try:
        for t in threads:
            t.join()
    except KeyboardInterrupt:
        global stop
        stop = True

        sys.stdout.write("\nStopping threads...\n")
        for t in threads:
            t.join()


if __name__ == "__main__":
    main()

実行結果

2026-03-05 18:09:24,262 ERROR wrong name! got worker-3, expected worker-2
2026-03-05 18:09:24,263 ERROR wrong name! got worker-2, expected worker-3
2026-03-05 18:09:25,716 ERROR wrong name! got worker-1, expected worker-0
2026-03-05 18:09:25,718 ERROR wrong name! got worker-0, expected worker-2
2026-03-05 18:09:25,717 ERROR wrong name! got worker-3, expected worker-1
2026-03-05 18:09:25,720 ERROR wrong name! got worker-2, expected worker-3
2026-03-05 18:09:27,687 ERROR wrong name! got worker-1, expected worker-0
2026-03-05 18:09:28,786 ERROR wrong name! got worker-0, expected worker-1

発生条件と影響

この問題は HTTP/1.1 では発生しません。1つの接続に複数のリクエストを多重化しているときにのみレスポンスの取り違えが発生します。

このバグを抱えていると、たとえば稀にあるユーザーに別のユーザーのデータを返してしまう、みたいな深刻な問題が発生する恐れがあります。

2025-11-06 に混入したバグが、 2026-02-03 に報告され、2026-02-04 に修正がリリースされました。メンテナの対応は素晴らしく早いものの、バグが混入してから見つかるまでに3ヶ月かかってしまっているのはユーザー数の少なさというデメリットが現れているのだと思います。

もともと requests で長時間 keep-alive すると稀にエラーになるのをなんとかしたいというモチベーションで niquests を使っていたので、社内で niquests を使おうとしていたあるサービスでは urllib3-future をバージョンアップするだけでなく HTTP/2 と HTTP/3 を無効にして運用することにしました。無効にするのは Session の引数で可能です。

# pool_maxsize は1ホストあたり接続をいくつまでプールするか。
# デフォルトはmaxsizeでも接続が足りなくなったら新規接続する(pool_block=False)ので、
# よっぽどのことがなければ pool_maxsize=1 で十分。
#
# keepalive_delayはHTTP/2用のオプションだが、HTTP/1.1では接続の寿命(新規接続して
# 何秒後に再利用をやめるか)として扱われる。
# 今の所idle_timeoutがないので、サーバー側のidle_timeoutにぶつかってエラーになる
# のを避けるためにkeepalive_delayを使っている。
# 60を採用したのは、nginxのデフォルトのidle_timeout 75sなのでそれより短い値として。
#
# requestsとの大きな違いとして、デフォルトでシステムのルート証明書を利用することに注意。
# ルート証明書のアップデートにはPythonのcertifiパッケージではなく Debian/Ubuntuの
# ca-certificates パッケージをアップデートすること。
#
# https://github.com/jawah/urllib3.future/issues/309
# 1つの接続に複数のリクエストを並行して処理できるHTTP2 or 3 で、並行にアクセスするとレスポンスが他の
# リクエストに行ってしまうという致命的な問題があった。 urllib3-future 2.15.902 で解決済みだが、
# 怖いのでHTTP/1.1に限定して利用する。
session = niquests.Session(pool_maxsize=1, keepalive_delay=60, disable_http2=True, disable_http3=True)
このブログに乗せているコードは引用を除き CC0 1.0 で提供します。