Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

教員ログインを有効化しても画面に出ない、原因は force-dynamic 書き忘れの静的プリレンダ

公開 読了時間 約3分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

認証を経ない公開ページに動的 API の呼び出しが無いと、Next.js はビルド時点の状態でそのルートを静的プリレンダして長期キャッシュするため、DB の値を後から変えても force-dynamic を明示しない限り画面には反映されない。

結論

認証を経ない公開ページに動的 API の呼び出しが無いと、Next.js はビルド時点の状態でそのルートを静的プリレンダし、CDN が s-maxage で長期キャッシュします。DB の値を後から変えても、force-dynamic を明示しない限り画面には反映されません。

/login は共通ログインを有効化済みの学校が1校以上あるかを DB から引いて教員ログイン UI の出し分けを決める設計でしたが、force-dynamic の指定が無く、ビルド時点(有効校0件)の HTML がそのまま固定配信されていました。

症状

system_admin が学校の共通パスワードを設定し teacher_login_enabled を有効にしても、/login に教員ログイン UI が現れませんでした。DB 側の値は正しく更新されていて、/ops/schools/... の管理画面側の表示にも反映されています。公開側の /login だけが古いままでした。

原因

/login は共通ログインが有効な学校の有無を毎リクエスト DB から引いて教員モードを出し分ける Server Component です。ただし force-dynamic の指定が無かったため、Next.js はこの公開ルートを他の静的ページと同様、ビルド時点の状態で静的プリレンダしました。ビルド実行時点では有効化されている学校が0件だったため、「教員ログインなし」の HTML がそのまま固定され、s-maxage=31536000(1年)の長期キャッシュが付与されました。

本番のレスポンスヘッダーで x-nextjs-prerender:1 とキャッシュの HIT を確認し、静的プリレンダが原因であることを特定しました。

直す

/loginexport const dynamic = "force-dynamic" を追加し、毎リクエスト DB を引く動的レンダリングに固定しました。

export const dynamic = "force-dynamic";

あわせて、学校の共通パスワードを設定・解除するアクション(setSchoolTeacherPasswordAction / clearSchoolTeacherPasswordAction)側でも revalidatePath("/login") を呼び、有効化直後の反映を確実にしています。

revalidatePath(`/ops/schools/${schoolId}/edit`);
revalidatePath(`/ops/schools/${schoolId}`);
revalidatePath("/login");

force-dynamic が本体の対策で、revalidatePath は動的化と二重の安全策です。片方だけでは、force-dynamic を将来誤って外した場合や、別の経路で teacher_login_enabled が変わった場合に同じ症状が再発します。

気づけなかった理由

/login の教員ログインの出し分けロジック自体は、コードレビューで見ても「有効な学校があれば UI を出す」という条件として正しく書けています。ローカル開発環境では毎回動的にレンダリングされるため、この分岐がキャッシュに固定されるという事象は再現しません。

本番で CDN のキャッシュを経由して初めて、ビルド時点の HTML が長期間配信され続けるという症状が現れます。ロジックが正しいことと、そのロジックが実行されるタイミングが正しいことは別の確認が要る、という話でした。

同じ Next.js のキャッシュまわりで、ミドルウェアの許可リストが原因だった話は 認証ミドルウェアが robots.txt をログインへリダイレクトする に書いています。

よくある質問

Q1なぜ /login だけ静的化されていたのですか?

/login は動的 API を呼ばない Server Component として書かれていました。force-dynamic の指定が無い公開ルートは、Next.js がビルド時点の状態で静的プリレンダします。ビルド実行時点では共通ログインが有効な学校が0件だったため、教員ログイン UI が出ない状態の HTML がそのまま固定されました。

Q2どうやって原因を突き止めたのですか?

本番のレスポンスヘッダーを確認し、x-nextjs-prerender:1 が付いていること、s-maxage=31536000(1年)で長期キャッシュされていることを見ました。DB側の値は正しく更新されているのに公開画面だけが古いままだったため、キャッシュ層で止まっていると判断できました。

Q3revalidatePath("/login") だけでは足りないのですか?

根本原因は force-dynamic が無く静的プリレンダされること自体にあるため、revalidatePath だけでは force-dynamic を将来外した場合や別経路で値が変わった場合に同じ症状が再発します。force-dynamic を本体の対策にし、revalidatePath は有効化直後の反映を確実にする二重の安全策として残しています。

Q4教員ログイン UI の出し分けはどこで判定していますか?

公開ログイン画面(/login)が、共通ログインを有効化済みの学校が1校以上あるかを毎リクエスト DB から引いて判定します。この判定自体は force-dynamic の有無に関わらず正しく書けていて、実行されるタイミングだけがキャッシュにより固定されていました。

この記事の根拠

  • TypeScriptファイル 1〜20行目コミット fb01ce6
  • TypeScriptファイル 316〜355行目コミット fb01ce6

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。