Cloudflare Workers × better-auth: hanging promise で「特定ユーザーだけ全滅」する
リクエスト中に作った promise を isolate 寿命でキャッシュすると、クライアント切断後に永遠に pending(hanging promise)になりうる。better-auth の遅延初期化がその罠に嵌ると、同じ isolate を掴んだユーザーだけ認証が永久ハングする。
どんなときに使う?
- Workers + better-auth(や類似の遅延初期化ライブラリ)で、「特定の人だけ全リクエストが応答しない/ブラウザ再起動で直る」
- zone ログは
499、Workers invocation はcanceledなのに、静的アセットは同じ接続で返っている - モジュール/インスタンスに初期化 promise をキャッシュしている
やり方(防御の 3 層)
原因の骨格: workerd は、リクエストの IoContext に属する promise を、クライアント切断後に resolve/reject しない。その promise を isolate 共有キャッシュに載せると、以後その isolate 上の await が全部止まる。
- 危険な動的 import を先回りで埋める
例:node:async_hooksのAsyncLocalStorageをグローバルで静的 import し、better-auth が遅延解決する前に共有コンテキストへセットする(バージョン依存の内部キーなのでアップグレード時は要確認)。 - 残る遅延初期化を
ctx.waitUntilに係留するwaitUntilは切断後も IoContext を延命するので、初期化を担いだリクエストが abort されても完了できる。係留側にもタイムアウト(hang 検知)を付ける。 - API 呼び出しに hang 検知とインスタンス作り直し
Promise.raceで数秒待っても返らなければキャッシュを捨てて再生成。回復しなければ未認証扱いで応答し、ハングを伝播させない。破棄は「自分が掴んでいたインスタンス」に限定し、並行リクエストの競合に注意。
診断の近道: 段階ログで「どこまで通ったか」を見る/NetLog で h2 PING が生きているか確認する。
注意点
- Abort や「前の fetch をキャンセルする最適化」が、初期化中断の頻度を上げる
- 公式修正前のライブラリでは外側の多層防御が現実的(better-auth には upstream issue 報告あり)
- 自分のコードでも「リクエスト中の promise をモジュール/isolate キャッシュに載せない」が一般則。載せるなら waitUntil か hang 検知を必須にする