Skip to main content
技術

Cloudflare Pages Functions + Better Auth で認証実装に失敗したときの切り分け手順と解決方法

Cloudflare Pages Functions で Better Auth を使った認証実装中に、`node:async_hooks`、`get-session` の null 応答、SPA ルートの 404 に詰まった実例を整理する。失敗の流れ、切り分け方、最終的な解決方法までまとめた。

Cloudflare Pages Functions + Better Auth で認証実装に失敗したときの切り分け手順と解決方法

はじめに

Cloudflare Pages Functions で Better Auth を使って認証機能を実装すると、想像以上に「認証そのもの」以外のところで詰まることがある。

最初は単純なログイン不具合に見えた。
しかし実際には、問題は一つではなかった。

  • Better Auth が node:async_hooks を読もうとして 500 になる
  • get-session200 でも null を返し、フロントが落ちる
  • /login の再読み込みで 404 になる

この記事ではこの失敗事例をそのまま材料にして、Cloudflare Pages + Better Auth の認証実装で何をどう切り分けるべきか、そして最終的にどう解決したかをまとめる。

なお、ローカル開発環境での認証テスト方法は「Cloudflare Pages + Better Auth のローカル認証テスト手順」にまとめている。
構成全体の設計については「React + Cloudflare Pages + Turso + Better Auth の構成」も合わせて参照してほしい。

最初に起きた症状

開発環境で /login を開き、Create Account を押すと、画面上は「アカウント作成に失敗しました」と表示された。

ブラウザの Network を確認すると、次のようなリクエストが失敗していた。

  • GET /api/auth/get-session500
  • POST /api/auth/sign-up/email500
  • GET /api/admin/session500

この時点では、フロントの入力値が悪いのか、auth API の base URL が悪いのか、DB が悪いのか、まだ分からない。

まず見るべきは Browser Console ではなく wrangler のログ

認証で詰まったとき、多くの場合ブラウザ Console だけでは足りない。

ブラウザ側では、せいぜい 500 Internal Server Error までしか見えなかった。拡張機能由来の chrome-extension://... ログも混ざっていて、ノイズも多い状態だった。

一方で、wrangler pages dev のターミナルログを見ると、次の原因がすぐに見つかった。

Error: No such module "node:async_hooks"

つまり、本当の原因は Better Auth のサーバー側ハンドラが Cloudflare Pages Functions 上で落ちていたことだった。

原因1: Better Auth が node:async_hooks を要求していた

このアプリでは最初、Workers ネイティブ構成を保ちたくて wrangler.tomlnodejs_compat を入れていなかった。

しかし、実際にインストールされていた Better Auth は内部的に @better-auth/core/async_hooks を使う経路があり、そこで node:async_hooks を読みに行っていた。

結果として、Cloudflare Pages Functions 側で Node 互換が有効になっていないため、auth handler が 500 になっていた。

対応

Workers ネイティブを守るために alias で逃がす案も検討したが、保守性を考えて nodejs_compat を採用した。

compatibility_flags = ["nodejs_compat"]

ただし、ここで1つ注意点がある。

wrangler.toml に書くだけでは、ローカルの pages dev 起動コマンドで期待どおり反映されない場面があった。そのため、開発環境では起動時に明示的にフラグを付ける形で確認した。

npm run build
npx wrangler pages dev dist --compatibility-date=2026-04-01 --compatibility-flags=nodejs_compat --port 8790

これで GET /api/auth/get-session 500POST /api/auth/sign-up/email 500 はまず一段階解消した。

原因2: get-session200 でも null を返し、フロントが落ちた

次に起きたのは認証 API 自体は 500 から抜けたのに、画面が次のエラーで落ちる問題だった。

Cannot read properties of null (reading 'data')

これはフロント側の route guard が、/api/auth/get-session の戻り値を楽観的に読みすぎていたことが原因だった。

Better Auth の get-session は、未ログイン時に 200data: null を返し得る。

しかしフロント側が payload.data を前提に読んでいたため、ここでクラッシュしていた。

対応

  • get-session のレスポンスを型ガードで確認する
  • null のときは未ログイン扱いで安全に流す
  • テストも追加する

認証 API が 500 でなくても、200 + null を正しく扱えないとログイン画面自体が壊れる。

原因3: /login の再読み込みで 404 になった

次に見つかったのは認証そのものではなく、Cloudflare Pages の SPA ルーティング問題だった。

症状はこうだ。

  • /login へのクライアント遷移はできる
  • しかし /login を直接開く、または再読み込みすると 404 になる

wrangler 側のログには、次のように出ていた。

GET /login 404 Not Found

これは React Router の問題ではなく、Cloudflare Pages が /login を静的ファイルとして探してしまっていたのが原因だ。

対応

SPA fallback として _redirects を追加した。

/* /index.html 200

これを public/_redirects として置いて build し、dist/_redirects に反映させることで、/login の直アクセスと再読み込みでも index.html にフォールバックできるようになった。

最終的にどう確認したか

問題を順に潰したあと、開発環境では次の状態まで確認できた。

  • GET /api/auth/get-session200
  • POST /api/auth/sign-up/email は成功すると 200
  • パスワードが短いと 400 BAD_REQUEST で適切に弾かれる
  • ログイン後の一般ユーザーは GET /api/admin/session403 Forbidden
  • これは失敗ではなく、「認証は通っているが admin 権限がない」という正常な認可結果

確認できたことは、次のとおりだ。

  • サインアップできる
  • ログインできる
  • セッション取得できる
  • member と admin の認可境界が機能している

なお、admin 画面そのものを確認するには、作成したユーザーを DB 側で admin に昇格する必要がある。

update user
set role = 'admin'
where email = 'admin-test@example.com';

Cloudflare Pages + Better Auth の認証確認で見るべきもの

今回の失敗から、開発環境では次の3つを必ずセットで見るべきだと分かった。

1. Browser Network

認証機能では、最低限これを見る。

  • GET /api/auth/get-session
  • POST /api/auth/sign-up/email
  • POST /api/auth/sign-in/email
  • GET /api/admin/session

見るべきステータスは次のとおりだ。

  • 200: 正常、または未ログインでも null payload の可能性あり
  • 400: 入力バリデーションエラー
  • 401: 未ログイン
  • 403: 権限不足
  • 500: サーバー例外

2. wrangler pages dev のターミナルログ

Cloudflare Pages Functions の本当の例外はここに出る。

今回の node:async_hooks も、ブラウザよりこちらで原因が明確に分かった。

3. フロント側の route guard 実装

認証 API が正しくても、フロントがレスポンス形を誤解していれば画面は壊れる。特に get-session のように 200 + null を返し得る API は要注意だ。

今回の教訓

1. 認証不具合は一つとは限らない

最初の 500 を直しても、次にフロントの null 応答処理や SPA fallback の不足が見つかった。認証周りは連鎖的に問題が出やすい。

2. nodejs_compat は「理想」より「現実」で判断する

Cloudflare Pages Functions で Better Auth を使うなら、まず nodejs_compat を前提に考えた方が現実的だ。Workers ネイティブ構成を守るコストが高い場面では、保守性を優先した方がよい。

3. 403 は失敗ではない

ログイン後に GET /api/admin/session 403 が出ると不安になるが、これは一般ユーザーが admin ではないことを示す正常な認可結果だ。
認証と認可は分けて見る必要がある。

4. SPA の再読み込み問題は認証問題に見えやすい

/login 再読み込みで落ちると認証不具合に見えるが、実際には _redirects 不足のような Pages 側のルーティング問題であることもある。

まとめ

Cloudflare Pages Functions + Better Auth の認証実装で問題が起きたときは次の順で切り分けるのが有効だ。

  1. wrangler のターミナルログで 500 の本当の原因を確認する
  2. nodejs_compat の必要性を判断する
  3. get-session200 + null をフロントが安全に扱えているかを見る
  4. SPA ルートの直アクセスと再読み込みで 404 になっていないか確認する
  5. 最後に 403 を含めた認可結果を正常系として整理する

認証機能の実装はコードを書いて終わりではない。
Cloudflare Pages Functions、Better Auth、DB、フロント側の route guard、SPA fallback まで含めて、初めて安定して動く。

もしこれから Cloudflare Pages + Better Auth を使うなら、「どう実装するか」だけでなく、「失敗したときに何をどう見るか」まで最初から決めておくことを勧める。